diff --git a/AUDIO_PLAYBACK_TESTING.md b/AUDIO_PLAYBACK_TESTING.md new file mode 100644 index 0000000..cbbc67b --- /dev/null +++ b/AUDIO_PLAYBACK_TESTING.md @@ -0,0 +1,156 @@ +# Testing Lua audio playback + +This guide tests issue #315 with a real WAV or MP3. The examples use the +T-Deck environment and the provided `scripts/audio_test.lua` app. + +## Build and flash + +Build and upload the branch as usual: + +```sh +pio run -e LilyGo_TDeck_companion_radio_touch +pio run -e LilyGo_TDeck_companion_radio_touch -t upload +``` + +Keep the serial monitor available for `[LUAAPP]` or storage errors: + +```sh +pio device monitor -b 115200 +``` + +## Quick MP3 test from SD + +1. Use a FAT32 SD card and create these paths on it: + + ```text + /Music/test.mp3 + /meshcomod/apps/audio_test.lua + ``` + +2. Copy `scripts/audio_test.lua` from this repository to the second path. Rename + your MP3 to `test.mp3`, or edit `SD_PATH` near the top of the Lua file. +3. Insert the card before boot, then reboot the T-Deck. +4. Enable **Settings > Sound** and set volume above zero. +5. Open **Apps > audio_test**. The screen should report + `audio=true`, `wav=true`, and `mp3=true`. +6. Press **Play**. Expected status: + + ```text + State: playing + Source: sd Format: mp3 + ``` + +7. Exercise **Pause**, **Resume**, and **Stop**. Pressing **Play** while already + playing must restart/replace the current track without overlapping it. +8. Start playback and leave the app. Audio must stop immediately when the app + closes. + +The direct SD API used by the test is: + +```lua +local ok, err = wada.audio.play("sd:/Music/test.mp3") +``` + +`sd:` paths are absolute from the physical card root. Empty segments, +backslashes, trailing slashes, `.` and `..` are rejected. + +## Test app-local/internal storage + +A plain filename is resolved inside the current app's private directory. For +`audio_test.lua`, the logical files are: + +```text +/apps/audio_test.lua +/apps/audio_test.d/test.mp3 +``` + +On T-Deck and Pager, internal app storage is SPIFFS. The least destructive way +to populate it is the repository's serial uploader: + +1. Power off, remove the SD card, and reboot. This forces T-Deck app storage to + the internal SPIFFS backend. +2. Make a short internal-storage fixture. T-Deck has a 3.375 MB SPIFFS partition + shared with settings and history, so use the full song for SD testing and a + small clip here: + + ```sh + ffmpeg -y -i "/path/to/your/file.mp3" \ + -t 10 -ac 1 -ar 22050 -b:a 48k \ + /tmp/wadamesh-audio-test.mp3 + ``` + +3. Find the serial port with `pio device list`. +4. Install `pyserial` once if needed: + + ```sh + python3 -m pip install pyserial + ``` + +5. Upload the app and short MP3. Replace the port as needed: + + ```sh + python3 scripts/sideload_app.py \ + --port /dev/cu.usbmodem101 \ + --remote /apps/audio_test.lua \ + scripts/audio_test.lua + + python3 scripts/sideload_app.py \ + --port /dev/cu.usbmodem101 \ + --remote /apps/audio_test.d/test.mp3 \ + --reboot \ + /tmp/wadamesh-audio-test.mp3 + ``` + +6. Open **Apps > audio_test** after reboot. With no card inserted, the app + selects `test.mp3`; press **Play**. +7. Expected status is `Source: app Format: mp3`. + +The app-local API is simply: + +```lua +local ok, err = wada.audio.play("test.mp3") +``` + +Tanmatsu uses internal FFat and T-Display P4 uses internal LittleFS. The same +serial destination `/apps/audio_test.d/test.mp3` follows the active internal +backend, so no Lua code changes are needed. Direct `sd:` playback is shown only +when `wada.sys.caps().audio_sd` is true. + +Do not use `uploadfs` on a device with data you need to keep: a filesystem-image +upload replaces the internal storage partition, including settings and history. + +## Error and lifecycle checks + +- Disable Sound, then press **Play**: expect `Command: muted`. +- Rename the file or remove the card: expect `not found` or `no sd`. +- Try a non-WAV/MP3 extension: expect `unsupported format`. +- Remove the card during SD playback only after the baseline test. Playback must + enter `error` or stop without rebooting; reinserting the card should allow a + later play after the normal SD recovery delay. +- Receive a mesh notification during playback: it must not start a second audio + stream over the media track. +- Repeatedly open/play/close the app: each close must release the worker, codec, + amplifier, file handle, and storage lease. + +## Host decoder regression test + +The standalone test validates the pinned decoder and the same rolling 16 KB +input algorithm used by firmware: + +```sh +c++ -std=c++17 -O2 test/test_minimp3_decoder.cpp -o /tmp/test_minimp3_decoder +/tmp/test_minimp3_decoder /path/to/your/file.mp3 +``` + +A successful run prints decoded frames, samples per channel, sample rate, +channels, and bitrate changes. The implementation has also been checked with +CBR, VBR, leading ID3v2, trailing ID3v1, and trailing APEv2 metadata. + +## Supported formats + +- WAV: PCM, 16-bit, mono or stereo, 8-48 kHz. +- MP3: MPEG Layer III, mono or stereo, 8-48 kHz, CBR or VBR, with common ID3 and + APEv2 metadata. + +Stereo is downmixed to the device's mono speaker path. `status()` reports +`stopped`, `playing`, `paused`, `ended`, or `error`. diff --git a/DEVICES.md b/DEVICES.md index a863ef6..4820503 100644 --- a/DEVICES.md +++ b/DEVICES.md @@ -23,6 +23,10 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases - **T-Deck**: the everything device: touch, physical keyboard, trackball cursor or d-pad navigation, microSD (deep 5000-message chat history, map tile packs, data storage), GPS on the Plus, notification sounds through the I2S speaker. + Tap Sym or Alt for one symbol, or double-tap either to lock the symbol layer; + this needs [LilyGO keyboard-controller firmware with raw matrix mode](https://github.com/Xinyuan-LilyGO/T-Deck/tree/master/examples/Keyboard_ESP32C3) + (June 2025 or newer). Older controller firmware keeps normal typing and + reports the unavailable latch mode on Serial. - **Heltec V4 + TFT**: touch UI with the on-screen keyboard; the optional Expansion Kit adds environment sensors (home-screen chart) and a piezo buzzer. V4.3 boards get the switchable high-gain receive LNA toggle. @@ -33,6 +37,8 @@ USB) and the [GitHub releases](https://github.com/ALLFATHER-BV/wadamesh/releases - **T-Lora Pager**: no touchscreen at all — the QWERTY keyboard and the rotary encoder drive everything (Alt+turn free-scrolls a page; see [TLORA_PAGER_SHORTCUTS.md](TLORA_PAGER_SHORTCUTS.md) for the full key map). + Tap Fn/Alt for one symbol or double-tap it to lock the symbol layer; physical + hold combinations keep their existing behavior. GPS, microSD, keyboard backlight, lock screen and notification sound through the onboard codec and amp all work. Two builds, one per radio: LR1121 and SX1262 — flashing the wrong one leaves you with no radio, so check the label diff --git a/NOTICE b/NOTICE index e5b4be9..711abdc 100644 --- a/NOTICE +++ b/NOTICE @@ -54,6 +54,13 @@ ed25519 (bundled in lib/ed25519) Orson Peters / orlp — public domain (Creative Commons Zero / zlib) https://github.com/orlp/ed25519 +minimp3 (bundled in lib/minimp3) + lieff/minimp3 contributors — CC0 1.0 Universal + https://github.com/lieff/minimp3 + Pinned at ea99364f61c14656440e8d77e9c233ccf3124633. The bundled header has + a small opt-in scratch-storage hook so the decoder workspace can live in + PSRAM; default upstream behavior is unchanged when that hook is not used. + AsyncElegantOTA (vendored in arch/esp32/AsyncElegantOTA) Copyright (c) Ayush Sharma — MIT License diff --git a/README.md b/README.md index 60ea0cd..2946c56 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ the transports, …) are dropped from the lib via `-DMC_VENDORED_TOUCH_APP` so t aren't compiled twice. The build is byte-identical to the original in-tree meshcomod firmware. +Lua apps use the on-device SDK described in [LUA_APPS.md](LUA_APPS.md). For +WAV/MP3 playback, including a ready-to-sideload transport test app and exact SD +and internal-storage paths, see [AUDIO_PLAYBACK_TESTING.md](AUDIO_PLAYBACK_TESTING.md). + ## Build [PlatformIO](https://platformio.org/) pulls the core fork and all libraries diff --git a/TLORA_PAGER_PORT.md b/TLORA_PAGER_PORT.md index 08c1d8e..89916f2 100644 --- a/TLORA_PAGER_PORT.md +++ b/TLORA_PAGER_PORT.md @@ -225,8 +225,8 @@ WASD-panning). `updatePagerAltShiftChord()` toggles Caps Lock ONLY while a text field is actually being edited (same "ta" derivation `handleHwKey()`'s TLORA_PAGER branch already uses to tell a bound-but-unfocused field apart from one - actually being edited), and jumps straight to Home (`navGoToMainTab`) - everywhere else. + actually being edited), and is a no-op everywhere else. Alt+Backspace is + the context-independent Home shortcut. 3. **@-mention contact picker made encoder-navigable** (commit `57802eb`) — the same unreachable-without-touch problem the accent box had, fixed the same way (`mentionNavRestyle()`/`mentionNavConfirm()` mirror @@ -622,9 +622,9 @@ existing boards. Notes: `pagerKeyboardConsumeAltShiftChord()`; `UITask.cpp`'s `updatePagerAltShiftChord()` toggles persistent Caps Lock ONLY while a text field is actually being edited (same "ta" derivation as `handleHwKey()`'s - TLORA_PAGER branch), and jumps straight Home (`navGoToMainTab`) everywhere - else — toggling Caps Lock with no field to see it change in was reported as - surprising/purposeless outside of typing. + TLORA_PAGER branch), and is a no-op everywhere else — toggling Caps Lock + with no field to see it change in was reported as surprising/purposeless + outside of typing. Alt+Backspace owns the Home shortcut. See `PagerKeyboard.cpp`'s matrix-legend comment for the one documented edge case (Alt+Shift+O or Alt+Shift+L all three held phantom-ghosts a stray 'q'/'a' — a diode-less-matrix limitation, not a bug, and not the @@ -751,7 +751,7 @@ existing boards. Notes: | Accent-variant popup keyboard nav | Fn+Space arms it; encoder walks variants; encoder click/Enter confirms; Backspace cancels (`s_accentnav_active`/`accentNavRestyle()`/`accentNavConfirm()`) | medium | done | | @-mention picker keyboard nav | Auto-arms the instant the list appears (no Fn+Space needed); same encoder walk/confirm/cancel as the accent popup (`s_mentionnav_active`/`mentionNavRestyle()`/`mentionNavConfirm()`) | medium | done | | Encoder short click on a focused bubble | Mirrors keyboard Enter — opens the same action menu instead of a plain ENTER a bubble doesn't react to | small | done | -| Fn+Shift chord effect | Toggles Caps Lock while editing a text field; jumps to Home (`navGoToMainTab`) everywhere else — `PagerKeyboard.cpp` only reports the chord, `UITask.cpp` decides the effect | small | done | +| Fn+Shift chord effect | Toggles Caps Lock while editing a text field; no-op everywhere else. Fn+Backspace jumps Home — `PagerKeyboard.cpp` only reports the chords, `UITask.cpp` decides the effects | small | done | | Map pan (WASD) + slider nudge (Q/E) | WASD pans the Map tab (`mapNudge()`, shared with Tanmatsu's Ctrl+Arrow); Q/E adjust a focused slider (moved off D/F to avoid colliding with WASD) | small | done | | App-drawer tile focus highlight | `NAV_ACCENTFOCUS_FLAG` makes `navFocusCb` use the tile's own accent tint instead of the generic white reverse-fill, on every board | small | done | | SX1262-variant env | `tlora_pager_sx1262_companion_radio_touch` — same board/pins, `CustomSX1262`/`CustomSX1262Wrapper` instead of the LR1121 classes | medium | done (worklist ⑩) | diff --git a/TLORA_PAGER_SHORTCUTS.md b/TLORA_PAGER_SHORTCUTS.md index 0b4abe3..3519edf 100644 --- a/TLORA_PAGER_SHORTCUTS.md +++ b/TLORA_PAGER_SHORTCUTS.md @@ -40,7 +40,7 @@ while editing a text field, where the keys type normally. | Gesture | Action | Keyboard equivalent | |---|---|---| -| Turn | Move focus to the next/previous item on screen | **Fn (Alt) tapped alone** moves forward one step (NEXT only — no keyboard way to go backward) | +| Turn | Move focus to the next/previous item on screen | none — encoder only | | Short click | Select / confirm the focused item | **Enter** | | Hold ~1 s, then release | **Back**: closes a popup → closes an open chat → goes Home → Esc (whichever applies first) | **Backspace held ~1 s** | | **Fn (Alt) + turn**, on a main tab | Move between the 5 main tabs (Mail / Contacts / Home / Map / Settings) | **M / C / H / A / S** jumps directly | @@ -93,29 +93,19 @@ text box → Send**, in that order, in both directions — nothing is skipped. Turning past the △ chip (toward the messages) always jumps straight to the newest message, regardless of channel or DM. -### Backspace and Fn (Alt) — leaving vs. catching up +### Backspace — catching up -These two keys are your shortcuts in and out of "reading mode" on this -screen, and they do different things depending on whether you've got unread -messages: +**Backspace (tap)** jumps to whichever message needs your attention: -- **Fn (Alt) tapped alone** — jumps straight to the **newest** message and - moves focus into the text box, ready to type a reply. This is the - deliberate "I'm done reading, let me respond" gesture — it works regardless - of where your focus currently is in the chat. -- **Backspace (tap)** — jumps to whichever message needs your attention: - - If there are unread messages, it jumps to the first one — right below the - **"NEW ----"** divider — and selects it, so you can then turn forward - through your unread messages in order, oldest-of-the-unread first. - - If nothing is unread, it jumps to the newest message instead (same - destination as Fn+Alt, but leaves focus on the message itself rather than - the text box). +- If there are unread messages, it jumps to the first one — right below the + **"NEW ----"** divider — and selects it, so you can then turn forward + through your unread messages in order, oldest-of-the-unread first. +- If nothing is unread, it jumps to the newest message instead (same + destination, with focus left on the message). Backspace's override above only applies while you're inside an open chat and *not* actively editing the text box — the usual rule from the top of this guide; with a field focused, Backspace deletes a character as normal. -Fn (Alt) tap's override fires the same way whether or not the text box -already has focus, since landing there is the whole point of the gesture. ## Keyboard layout @@ -128,7 +118,9 @@ a s d f g h j k l [Enter] [Space] ``` -Hold **Fn (Alt)** for numbers/symbols instead: +Hold **Fn (Alt)** for numbers/symbols, or tap it once to apply this layer to +the next key only. Double-tap Fn to lock the symbol layer; tap it again to +return to letters. ``` 1 2 3 4 5 6 7 8 9 0 @@ -162,7 +154,7 @@ Hold **Fn (Alt)** for numbers/symbols instead: | **Space** (tap) | Types a space | — | | **Space** (double-tap, within 250 ms) | Switches between English and your configured secondary keyboard layout | — | | **Space** (hold ~1 s) | — | Locks the screen (shows a "Locking…" progress bar; tapping any key cancels) | -| **Fn (Alt)** tapped alone (press+release, nothing else) | — | Moves focus to the next field/item — a keyboard-only substitute for a turn of the encoder. In an open chat: jumps to the newest message and focuses the text box instead — see [Chat screen](#chat-screen) | +| **Fn (Alt)** (tap / double-tap) | Next key uses symbols / lock symbols until tapped again | Same | The **BOOT** button (top of the device) instantly wakes the screen from idle-dim. It does *not* unlock a screen you've manually locked with the @@ -249,13 +241,14 @@ that changes keyboard language. | Input | Action | |---|---| | M / C / H / A / S (top-level main screens only) | Open Mail / Contacts / Home / Map / Settings | -| Turn encoder (or Fn tap = NEXT only) | Move focus | +| Turn encoder | Move focus | | Turn, at the loaded edge of a chat | Load more history, keep moving — only exits the list at the true oldest/newest message | | Click encoder (or Enter) | Select / confirm | | Hold encoder ~1s (or hold Backspace ~1s) | Back | | Fn + turn (main tab) | Switch tabs | | Fn + turn (page/chat) | Scroll | -| Fn tap alone | Next field (in a chat: jump to newest message + focus the text box) | +| Fn tap alone | Use the symbol layer for the next key | +| Fn double-tap | Lock the symbol layer; tap Fn again to unlock | | Hold Shift + letter | Momentary uppercase | | Fn + Shift (editing a field) | Toggle Caps Lock | | Fn + Shift (not editing a field) | Nothing | diff --git a/deploy/apps/README.md b/deploy/apps/README.md index 6e9b046..751dcfb 100644 --- a/deploy/apps/README.md +++ b/deploy/apps/README.md @@ -16,8 +16,10 @@ One Lua file plus a small manifest. Apps talk to the firmware through the `wada. API — `wada.ui` (widgets, colours, a text prompt), `wada.sys`, `wada.store` (persistence), `wada.timer`, and on the larger boards `wada.fs`, `wada.net`, `wada.crypto` and the writable half of `wada.mesh`. Supported boards also expose -read-only SD directory metadata through `wada.sd`; file contents and writes stay -inaccessible. There is no general-purpose filesystem or network access; the API +asynchronous WAV/MP3 playback through `wada.audio`; plain filenames come from the +app sandbox on internal flash, SD, or SD_MMC. Read-only physical-SD directory +metadata is available through `wada.sd`, and direct card playback uses an explicit +`sd:/...` path. There is no general-purpose filesystem or network access; the API is the whole surface, which is what makes reviewing tractable. It is documented at [wadamesh.com/sdk.html](https://wadamesh.com/sdk.html). diff --git a/deploy/apps/apps.json b/deploy/apps/apps.json index ee2da03..9e6b97d 100644 --- a/deploy/apps/apps.json +++ b/deploy/apps/apps.json @@ -21,8 +21,8 @@ { "id": "sdktest", "name": "SDK Test", - "ver": "1.6", - "desc": "Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private file access, read-only SD listing, crypto against published RFC vectors, channel discovery, and buttons that TRANSMIT test messages." + "ver": "1.7", + "desc": "Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private file access, audio API, read-only SD listing, crypto against published RFC vectors, channel discovery, and buttons that TRANSMIT test messages." }, { "id": "2048", @@ -33,14 +33,16 @@ { "id": "wardrive", "name": "Wardrive", - "ver": "1.0", - "desc": "LoRa coverage survey. Probes every 20s and logs each reply with GPS position, altitude and BOTH link directions (how well you heard them, how well they heard you) to a CSV. Reference app for wada.mesh.discover." + "ver": "1.1", + "desc": "LoRa coverage survey. Probes every 20s and logs each reply with GPS position, altitude and BOTH link directions (how well you heard them, how well they heard you) to a CSV. Reference app for wada.mesh.discover.", + "requires": "sdk_ext" }, { "id": "nearby", "name": "Nearby", "ver": "1.0", - "desc": "Your contacts on a real map, sorted by distance, with bearing and a live frame counter. Reference app for wada.map, wada.ui.list, wada.geo and app.on_packet." + "desc": "Your contacts on a real map, sorted by distance, with bearing and a live frame counter. Reference app for wada.map, wada.ui.list, wada.geo and app.on_packet.", + "requires": "sdk_ext" }, { "id": "gpscompass", diff --git a/deploy/apps/lang/18/bg.lang b/deploy/apps/lang/19/bg.lang similarity index 97% rename from deploy/apps/lang/18/bg.lang rename to deploy/apps/lang/19/bg.lang index 92bcace..21b7c3f 100644 --- a/deploy/apps/lang/18/bg.lang +++ b/deploy/apps/lang/19/bg.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: bg -# ver: 18 +# ver: 19 # name: Български # base: bg (device behind) (устройството изостава) @@ -826,7 +826,7 @@ TCP on TCP вкл TOUCH BETA TOUCH BETA TX TX Tab hotkeys — tap a row, then press a key Клавиши за раздела — докосни ред, после натисни клавиш -Tap Use to switch - the device reboots to apply. Натиснете „Избери“, за да превключите — устройството ще се рестартира. +Tap Use to switch - the device reboots to apply. Натиснете "Избери", за да превключите — устройството ще се рестартира. Tap Use to switch - the device reboots to apply. Language files live in /lang on the storage; edit them or add your own. Докосни «Избери» за смяна - устройството се рестартира. Езиковите файлове са в /lang в паметта; редактирай ги или добави свои. Tap a language; the device reboots to apply. Изберете език; устройството ще се рестартира. Taste the rainbow! Вкуси дъгата! @@ -977,8 +977,6 @@ tap to exit full screen докоснете за изход от цял екра zoom мащаб zoom %d мащаб %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. усилвател ~17 dB за тихи/отдалечени места; изключи в шумни. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/de.lang b/deploy/apps/lang/19/de.lang similarity index 97% rename from deploy/apps/lang/18/de.lang rename to deploy/apps/lang/19/de.lang index 60d5b56..1722419 100644 --- a/deploy/apps/lang/18/de.lang +++ b/deploy/apps/lang/19/de.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: de -# ver: 18 +# ver: 19 # name: Deutsch # base: de (device behind) (Gerät geht nach) @@ -452,7 +452,6 @@ Login OK (perms %u) Anmeldung OK (Rechte %u) Login failed Anmeldung fehlgeschlagen Logs Protokolle Longitude Längengrad -Lua Store Lua Store MHz MHz MQTT bridge MQTT-Brücke Map Karte @@ -694,7 +693,7 @@ SD card remounted SD-Karte neu eingebunden SD card removed SD-Karte entfernt SD data is unavailable - contacts and channels cannot be saved until the card is reinserted. SD-Daten nicht verfügbar – Kontakte und Kanäle können erst nach Einsetzen der Karte gespeichert werden. SD data is unavailable - identity, settings, contacts and channels cannot be saved until the card is reinserted. SD-Daten nicht verfügbar – Identität, Einstellungen, Kontakte und Kanäle können erst nach Einsetzen der Karte gespeichert werden. -SD data migration is incomplete. Identity and settings remain internal; use Copy internal data to SD to retry. SD-Datenübertragung unvollständig. Identität und Einstellungen bleiben intern; „Interne Daten auf SD kopieren“ zum erneuten Versuch verwenden. +SD data migration is incomplete. Identity and settings remain internal; use Copy internal data to SD to retry. SD-Datenübertragung unvollständig. Identität und Einstellungen bleiben intern; "Interne Daten auf SD kopieren" zum erneuten Versuch verwenden. SD download failed: %s SD-Download fehlgeschlagen: %s SD formatted - %s (MESHCOMOD) SD formatiert - %s (MESHCOMOD) SD is busy - close tools and retry SD ist belegt – Tools schließen und erneut versuchen @@ -797,7 +796,6 @@ Sleep ready Bereit zum Schlafen Slot %d cleared Slot %d gelöscht Slot empty Slot leer Small\nMedium\nLarge Klein\nMittel\nGroß -Snake Snake Snake — swipe or roll the trackball Snake — wischen oder Trackball rollen Sort Sortieren Sound Ton @@ -805,7 +803,6 @@ Sound off Ton aus Sound on Ton ein Sound: %s Ton: %s Start time Startzeit -Status Status Step 1 of 3 Schritt 1 von 3 Step 2 of 3 Schritt 2 von 3 Step 3 of 3 Schritt 3 von 3 @@ -818,7 +815,6 @@ Streams the device screen as a live picture you tap to control. Heavier; the pan Switch Wechseln Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot. Auf die externe Antenne wechseln?\n\nErst sicherstellen, dass wirklich eine Antenne an der Buchse steckt. Senden ohne Antenne kann das Funkmodul beschädigen.\n\nFällt bei jedem Neustart auf die interne Antenne zurück. Sync clock from system Uhr synchronisieren -System System System info Systeminfo TCP %s BLE %s\nWS clients %d GPS %s Buzzer %s TCP %s BLE %s\nWS-Clients %d GPS %s Summer %s TCP off TCP aus @@ -977,8 +973,6 @@ tap to exit full screen tippen zum Verlassen des Vollbilds zoom Zoom zoom %d Zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ~17 dB Verstärker für ruhige/abgelegene Gebiete; in lauten Zonen aus. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +994,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1007,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1033,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1041,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1050,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1062,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1079,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/el.lang b/deploy/apps/lang/19/el.lang similarity index 97% rename from deploy/apps/lang/18/el.lang rename to deploy/apps/lang/19/el.lang index 203ba0b..6e966fc 100644 --- a/deploy/apps/lang/18/el.lang +++ b/deploy/apps/lang/19/el.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: el -# ver: 18 +# ver: 19 # name: Ελληνικά # base: el (device behind) (συσκευή πίσω) @@ -977,8 +977,6 @@ tap to exit full screen πατήστε για έξοδο από πλήρη οθ zoom ζουμ zoom %d ζουμ %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ενισχυτής ~17 dB για ήσυχες/απομακρυσμένες περιοχές· κλείστε σε θορυβώδεις. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/es.lang b/deploy/apps/lang/19/es.lang similarity index 97% rename from deploy/apps/lang/18/es.lang rename to deploy/apps/lang/19/es.lang index fe56063..0b2cdbc 100644 --- a/deploy/apps/lang/18/es.lang +++ b/deploy/apps/lang/19/es.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: es -# ver: 18 +# ver: 19 # name: Español # base: es (device behind) (dispositivo atrasado) @@ -977,8 +977,6 @@ tap to exit full screen toca para salir de pantalla completa zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. amplificador ~17 dB para zonas tranquilas/remotas; apaga en zonas ruidosas. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/fr.lang b/deploy/apps/lang/19/fr.lang similarity index 97% rename from deploy/apps/lang/18/fr.lang rename to deploy/apps/lang/19/fr.lang index 7d5a664..8ccf214 100644 --- a/deploy/apps/lang/18/fr.lang +++ b/deploy/apps/lang/19/fr.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: fr -# ver: 18 +# ver: 19 # name: Français # base: fr (device behind) (appareil en retard) @@ -452,7 +452,6 @@ Login OK (perms %u) Connexion OK (perms %u) Login failed Échec de connexion Logs Journaux Longitude Longitude -Lua Store Lua Store MHz MHz MQTT bridge Pont MQTT Map Carte @@ -977,8 +976,6 @@ tap to exit full screen toucher pour quitter le plein écran zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ampli ~17 dB pour zones calmes/isolées ; désactiver en zone bruyante. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +997,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1010,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1036,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1044,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1053,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1065,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1082,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/hu.lang b/deploy/apps/lang/19/hu.lang similarity index 96% rename from deploy/apps/lang/18/hu.lang rename to deploy/apps/lang/19/hu.lang index 84dfac7..fa5188b 100644 --- a/deploy/apps/lang/18/hu.lang +++ b/deploy/apps/lang/19/hu.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: hu -# ver: 18 +# ver: 19 # name: Magyar # base: hu (device behind) (készülék hátulján) @@ -703,7 +703,7 @@ SD card remounted SD kártya újracsatolva SD card removed SD-kártya eltávolítva SD data is unavailable - contacts and channels cannot be saved until the card is reinserted. Az SD-kártya adatai nem érhetők el – a névjegyek és a csatornák nem menthetők a kártya újbóli behelyezéséig. SD data is unavailable - identity, settings, contacts and channels cannot be saved until the card is reinserted. Az SD-kártya adatai nem érhetők el – az azonosító, a beállítások, a névjegyek és a csatornák nem menthetők a kártya újbóli behelyezéséig. -SD data migration is incomplete. Identity and settings remain internal; use Copy internal data to SD to retry. Az SD-kártya adatainak áthelyezése nem teljes. Az azonosító és a beállítások továbbra is a belső tárhelyen vannak; a folytatáshoz használd a „Belső adatok másolása SD-kártyára” lehetőséget. +SD data migration is incomplete. Identity and settings remain internal; use Copy internal data to SD to retry. Az SD-kártya adatainak áthelyezése nem teljes. Az azonosító és a beállítások továbbra is a belső tárhelyen vannak; a folytatáshoz használd a "Belső adatok másolása SD-kártyára" lehetőséget. SD download failed: %s SD-letöltés sikertelen: %s SD formatted - %s (MESHCOMOD) SD formázás - %s (MESHCOMOD) SD is busy - close tools and retry Az SD-kártya foglalt – zárd be az eszközöket, majd próbáld újra. @@ -812,7 +812,6 @@ Sleep ready Készen áll az alvásra Slot %d cleared %d hely kiürítve Slot empty Üres hely Small\nMedium\nLarge Kicsi\nKözepes\nNagy -Snake Snake Snake — swipe or roll the trackball Kígyó – pöccintsd vagy görgesd a görgetőgolyót Sort Sort Sound Hang @@ -997,8 +996,6 @@ tap to exit full screen koppintson a teljes képernyős kilépéshez zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ~17 dB erősítővel csendes/távoli helyeken; zajos helyeken kapcsold ki. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope Hatókör another region másik régió my region saját régió @@ -1065,11 +1062,10 @@ sensors érzékelők Auto-add on for %s — new %s land in Contacts automatically. Automatikus hozzáadás bekapcsolva: %s — az új %s automatikusan a Névjegyek közé kerülnek. Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed under the Open Database License (ODbL).\n\n Térkép adatok © OpenStreetMap közreműködői.\nopenstreetmap.org/copyright\nAz Open Database License (ODbL) licenc alatt.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Térkép stílus © OpenTopoMap (CC-BY-SA) — opentopomap.org\nTérkép adatok © OpenStreetMap közreműködői (ODbL) + SRTM.\n\n -How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. A csempék működése:\nA térkép 256×256 képpontos „slippy” csempékből épül fel. Csak a megtekintett területhez szükséges csempék töltődnek le — nincs tömeges előzetes letöltés.\n\nMivel az eszköz nem támogatja a HTTPS-t (a Wi-Fi elindítása után nincs elegendő heap memória), és a JPEG képek dekódolása sokkal kevesebb erőforrást igényel, mint a PNG-é, a csempék a wadamesh proxyn keresztül érkeznek: a proxy HTTPS-en keresztül lekéri a forrásból a PNG-t egy azonosító User-Agent használatával, JPEG formátumba kódolja át, majd eltárolja a gyorsítótárban. Az eszköz ezután minden csempét a saját flash memóriájában is eltárol, így egy csempét csak egyszer kell letölteni.\n\nAz Options → Reload tiles lehetőséggel újra letöltheted a jelenleg megtekintett terület csempéit, ha valamelyik hibásan jelenik meg. +How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. A csempék működése:\nA térkép 256×256 képpontos "slippy" csempékből épül fel. Csak a megtekintett területhez szükséges csempék töltődnek le — nincs tömeges előzetes letöltés.\n\nMivel az eszköz nem támogatja a HTTPS-t (a Wi-Fi elindítása után nincs elegendő heap memória), és a JPEG képek dekódolása sokkal kevesebb erőforrást igényel, mint a PNG-é, a csempék a wadamesh proxyn keresztül érkeznek: a proxy HTTPS-en keresztül lekéri a forrásból a PNG-t egy azonosító User-Agent használatával, JPEG formátumba kódolja át, majd eltárolja a gyorsítótárban. Az eszköz ezután minden csempét a saját flash memóriájában is eltárol, így egy csempét csak egyszer kell letölteni.\n\nAz Options → Reload tiles lehetőséggel újra letöltheted a jelenleg megtekintett terület csempéit, ha valamelyik hibásan jelenik meg. "%s" wants to send discovery probes.\n\nEach probe asks every node in range to reply, so it uses airtime on the whole local mesh, not just yours.\n\nNothing is sent under your name and no message is transmitted. "%s" felfedezési probe-ok küldését kéri.\n\nMinden probe válaszra kéri a hatótávolságon belüli node-okat, ezért a teljes helyi mesh hálózat airtime-ját használja, nem csak a tiédet.\n\nSemmi sem kerül elküldésre a nevedben, és üzenet sem kerül továbbításra. ? ? Auth failed Auth failed -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Csak közvetlen (0-hop) @@ -1078,16 +1074,12 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Szöveg megadása Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... Link lost Link lost MESH MESH Name (A-Z) Név (A–Z) Nearest first Legközelebbi elöl -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Legutóbbi üzenet Reload tiles in view Csempék újratöltése Repeaters Repeaters @@ -1098,13 +1090,8 @@ Show contacts Névjegyek megjelenítése Show coordinates Koordináták Show tile z/x/y Csempe z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Csempe hiba keret -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite tap to cycle off / on / auto tap to cycle off / on / auto #7A7F87 Scanning… nothing has answered yet# #7A7F87 Scanning… nothing has answered yet# Export crash report (%uK) Hibajelentés exportálása (%uK) @@ -1116,3 +1103,21 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. A beta_%d telepítése?\nEz leminősíti a firmware-t és újraindul. +Other (hidden) network… Other (hidden) network… +Paused Paused +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/it.lang b/deploy/apps/lang/19/it.lang similarity index 97% rename from deploy/apps/lang/18/it.lang rename to deploy/apps/lang/19/it.lang index e0350d1..49b3183 100644 --- a/deploy/apps/lang/18/it.lang +++ b/deploy/apps/lang/19/it.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: it -# ver: 18 +# ver: 19 # name: Italiano # base: it (device behind) (dispositivo indietro) @@ -452,7 +452,6 @@ Login OK (perms %u) Accesso OK (permessi %u) Login failed Accesso non riuscito Logs Log Longitude Longitudine -Lua Store Lua Store MHz MHz MQTT bridge Ponte MQTT Map Mappa @@ -977,8 +976,6 @@ tap to exit full screen tocca per uscire da schermo intero zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ampli ~17 dB per zone tranquille/remote; spegni in zone rumorose. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +997,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1010,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1036,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1044,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1053,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1065,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1082,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/nl.lang b/deploy/apps/lang/19/nl.lang similarity index 97% rename from deploy/apps/lang/18/nl.lang rename to deploy/apps/lang/19/nl.lang index a7f4f93..cc0e72d 100644 --- a/deploy/apps/lang/18/nl.lang +++ b/deploy/apps/lang/19/nl.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: nl -# ver: 18 +# ver: 19 # name: Nederlands # base: nl (device behind) (apparaat loopt achter) @@ -452,7 +452,6 @@ Login OK (perms %u) Inloggen OK (rechten %u) Login failed Inloggen mislukt Logs Logboeken Longitude Lengtegraad -Lua Store Lua Store MHz MHz MQTT bridge MQTT-brug Map Kaart @@ -797,7 +796,6 @@ Sleep ready Klaar voor slaap Slot %d cleared Slot %d gewist Slot empty Slot leeg Small\nMedium\nLarge Klein\nMiddel\nGroot -Snake Snake Snake — swipe or roll the trackball Snake — veeg of rol de trackball Sort Sorteer Sound Geluid @@ -805,7 +803,6 @@ Sound off Geluid uit Sound on Geluid aan Sound: %s Geluid: %s Start time Starttijd -Status Status Step 1 of 3 Stap 1 van 3 Step 2 of 3 Stap 2 van 3 Step 3 of 3 Stap 3 van 3 @@ -977,8 +974,6 @@ tap to exit full screen tik om volledig scherm te verlaten zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ~17 dB versterker voor stille/afgelegen gebieden; uit in drukke plekken. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +995,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1008,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1034,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1042,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1051,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1063,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1080,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/pt-br.lang b/deploy/apps/lang/19/pt-br.lang similarity index 97% rename from deploy/apps/lang/18/pt-br.lang rename to deploy/apps/lang/19/pt-br.lang index e39cb21..2bcda5e 100644 --- a/deploy/apps/lang/18/pt-br.lang +++ b/deploy/apps/lang/19/pt-br.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: pt-br -# ver: 18 +# ver: 19 # name: Português (BR) # base: pt-br (device behind) (dispositivo atrasado) @@ -805,7 +805,6 @@ Sound off Som desligado Sound on Som ligado Sound: %s Som: %s Start time Hora de início -Status Status Step 1 of 3 Passo 1 de 3 Step 2 of 3 Passo 2 de 3 Step 3 of 3 Passo 3 de 3 @@ -977,8 +976,6 @@ tap to exit full screen toque para sair da tela cheia zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. amplificador ~17 dB para áreas silenciosas/remotas; desligue em locais ruidosos. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +997,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1010,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1036,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1044,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1053,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1065,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1082,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/ro.lang b/deploy/apps/lang/19/ro.lang similarity index 97% rename from deploy/apps/lang/18/ro.lang rename to deploy/apps/lang/19/ro.lang index 6967b9d..4679e46 100644 --- a/deploy/apps/lang/18/ro.lang +++ b/deploy/apps/lang/19/ro.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: ro -# ver: 18 +# ver: 19 # name: Română # base: ro (device behind) (dispozitiv în urmă) @@ -978,7 +978,6 @@ zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. amplificator ~17 dB pentru zone liniştite/izolate; opreşte-l în locuri zgomotoase. © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +999,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1012,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1038,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1046,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1055,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1067,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1084,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/ru.lang b/deploy/apps/lang/19/ru.lang similarity index 97% rename from deploy/apps/lang/18/ru.lang rename to deploy/apps/lang/19/ru.lang index ceb1ec3..5c8d5cc 100644 --- a/deploy/apps/lang/18/ru.lang +++ b/deploy/apps/lang/19/ru.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: ru -# ver: 18 +# ver: 19 # name: Русский # base: ru (device behind) (устройство отстаёт) @@ -977,8 +977,6 @@ tap to exit full screen нажмите для выхода из полноэкр zoom масштаб zoom %d масштаб %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. усилитель ~17 дБ для тихих/удалённых мест; выключайте в шумных. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/sr.lang b/deploy/apps/lang/19/sr.lang similarity index 97% rename from deploy/apps/lang/18/sr.lang rename to deploy/apps/lang/19/sr.lang index 0aab3e1..bd479b9 100644 --- a/deploy/apps/lang/18/sr.lang +++ b/deploy/apps/lang/19/sr.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: sr -# ver: 18 +# ver: 19 # name: Српски # base: sr (device behind) (уређај касни) @@ -826,7 +826,7 @@ TCP on TCP укљ TOUCH BETA TOUCH BETA TX TX Tab hotkeys — tap a row, then press a key Пречице картице — додирни ред, па притисни тастер -Tap Use to switch - the device reboots to apply. Додирните „Користи“ да промените — уређај ће се поново покренути. +Tap Use to switch - the device reboots to apply. Додирните "Користи" да промените — уређај ће се поново покренути. Tap Use to switch - the device reboots to apply. Language files live in /lang on the storage; edit them or add your own. Додирни «Користи» за промену - уређај се поново покреће. Датотеке језика су у /lang на меморији; уреди их или додај своје. Tap a language; the device reboots to apply. Изаберите језик; уређај ће се рестартовати. Taste the rainbow! Окуси дугу! @@ -977,8 +977,6 @@ tap to exit full screen додирните за излаз из целог ек zoom зум zoom %d зум %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. појачало ~17 dB за тиха/удаљена места; искључи у бучним. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/18/uk.lang b/deploy/apps/lang/19/uk.lang similarity index 97% rename from deploy/apps/lang/18/uk.lang rename to deploy/apps/lang/19/uk.lang index 4ebd00f..52d7572 100644 --- a/deploy/apps/lang/18/uk.lang +++ b/deploy/apps/lang/19/uk.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: uk -# ver: 18 +# ver: 19 # name: Українська # base: uk (device behind) (пристрій відстає) @@ -977,8 +977,6 @@ tap to exit full screen торкніться, щоб вийти з повное zoom масштаб zoom %d масштаб %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. підсилювач ~17 дБ для тихих/віддалених місць; вимикайте в галасливих. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. diff --git a/deploy/apps/lang/bg.lang b/deploy/apps/lang/bg.lang index 92bcace..46da469 100644 --- a/deploy/apps/lang/bg.lang +++ b/deploy/apps/lang/bg.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: bg -# ver: 18 +# ver: 19 # name: Български # base: bg (device behind) (устройството изостава) @@ -826,7 +826,7 @@ TCP on TCP вкл TOUCH BETA TOUCH BETA TX TX Tab hotkeys — tap a row, then press a key Клавиши за раздела — докосни ред, после натисни клавиш -Tap Use to switch - the device reboots to apply. Натиснете „Избери“, за да превключите — устройството ще се рестартира. +Tap Use to switch - the device reboots to apply. Натиснете "Избери", за да превключите — устройството ще се рестартира. Tap Use to switch - the device reboots to apply. Language files live in /lang on the storage; edit them or add your own. Докосни «Избери» за смяна - устройството се рестартира. Езиковите файлове са в /lang в паметта; редактирай ги или добави свои. Tap a language; the device reboots to apply. Изберете език; устройството ще се рестартира. Taste the rainbow! Вкуси дъгата! @@ -977,8 +977,6 @@ tap to exit full screen докоснете за изход от цял екра zoom мащаб zoom %d мащаб %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. усилвател ~17 dB за тихи/отдалечени места; изключи в шумни. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/de.lang b/deploy/apps/lang/de.lang index 60d5b56..e26299e 100644 --- a/deploy/apps/lang/de.lang +++ b/deploy/apps/lang/de.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: de -# ver: 18 +# ver: 19 # name: Deutsch # base: de (device behind) (Gerät geht nach) @@ -452,7 +452,6 @@ Login OK (perms %u) Anmeldung OK (Rechte %u) Login failed Anmeldung fehlgeschlagen Logs Protokolle Longitude Längengrad -Lua Store Lua Store MHz MHz MQTT bridge MQTT-Brücke Map Karte @@ -694,7 +693,7 @@ SD card remounted SD-Karte neu eingebunden SD card removed SD-Karte entfernt SD data is unavailable - contacts and channels cannot be saved until the card is reinserted. SD-Daten nicht verfügbar – Kontakte und Kanäle können erst nach Einsetzen der Karte gespeichert werden. SD data is unavailable - identity, settings, contacts and channels cannot be saved until the card is reinserted. SD-Daten nicht verfügbar – Identität, Einstellungen, Kontakte und Kanäle können erst nach Einsetzen der Karte gespeichert werden. -SD data migration is incomplete. Identity and settings remain internal; use Copy internal data to SD to retry. SD-Datenübertragung unvollständig. Identität und Einstellungen bleiben intern; „Interne Daten auf SD kopieren“ zum erneuten Versuch verwenden. +SD data migration is incomplete. Identity and settings remain internal; use Copy internal data to SD to retry. SD-Datenübertragung unvollständig. Identität und Einstellungen bleiben intern; "Interne Daten auf SD kopieren" zum erneuten Versuch verwenden. SD download failed: %s SD-Download fehlgeschlagen: %s SD formatted - %s (MESHCOMOD) SD formatiert - %s (MESHCOMOD) SD is busy - close tools and retry SD ist belegt – Tools schließen und erneut versuchen @@ -797,7 +796,6 @@ Sleep ready Bereit zum Schlafen Slot %d cleared Slot %d gelöscht Slot empty Slot leer Small\nMedium\nLarge Klein\nMittel\nGroß -Snake Snake Snake — swipe or roll the trackball Snake — wischen oder Trackball rollen Sort Sortieren Sound Ton @@ -805,7 +803,6 @@ Sound off Ton aus Sound on Ton ein Sound: %s Ton: %s Start time Startzeit -Status Status Step 1 of 3 Schritt 1 von 3 Step 2 of 3 Schritt 2 von 3 Step 3 of 3 Schritt 3 von 3 @@ -818,7 +815,6 @@ Streams the device screen as a live picture you tap to control. Heavier; the pan Switch Wechseln Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot. Auf die externe Antenne wechseln?\n\nErst sicherstellen, dass wirklich eine Antenne an der Buchse steckt. Senden ohne Antenne kann das Funkmodul beschädigen.\n\nFällt bei jedem Neustart auf die interne Antenne zurück. Sync clock from system Uhr synchronisieren -System System System info Systeminfo TCP %s BLE %s\nWS clients %d GPS %s Buzzer %s TCP %s BLE %s\nWS-Clients %d GPS %s Summer %s TCP off TCP aus @@ -977,8 +973,6 @@ tap to exit full screen tippen zum Verlassen des Vollbilds zoom Zoom zoom %d Zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ~17 dB Verstärker für ruhige/abgelegene Gebiete; in lauten Zonen aus. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +994,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1007,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1033,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1041,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1050,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1062,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1079,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/el.lang b/deploy/apps/lang/el.lang index 203ba0b..ee4ade2 100644 --- a/deploy/apps/lang/el.lang +++ b/deploy/apps/lang/el.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: el -# ver: 18 +# ver: 19 # name: Ελληνικά # base: el (device behind) (συσκευή πίσω) @@ -977,8 +977,6 @@ tap to exit full screen πατήστε για έξοδο από πλήρη οθ zoom ζουμ zoom %d ζουμ %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ενισχυτής ~17 dB για ήσυχες/απομακρυσμένες περιοχές· κλείστε σε θορυβώδεις. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/es.lang b/deploy/apps/lang/es.lang index fe56063..ccc5c3e 100644 --- a/deploy/apps/lang/es.lang +++ b/deploy/apps/lang/es.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: es -# ver: 18 +# ver: 19 # name: Español # base: es (device behind) (dispositivo atrasado) @@ -977,8 +977,6 @@ tap to exit full screen toca para salir de pantalla completa zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. amplificador ~17 dB para zonas tranquilas/remotas; apaga en zonas ruidosas. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/fr.lang b/deploy/apps/lang/fr.lang index 7d5a664..038ab8a 100644 --- a/deploy/apps/lang/fr.lang +++ b/deploy/apps/lang/fr.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: fr -# ver: 18 +# ver: 19 # name: Français # base: fr (device behind) (appareil en retard) @@ -452,7 +452,6 @@ Login OK (perms %u) Connexion OK (perms %u) Login failed Échec de connexion Logs Journaux Longitude Longitude -Lua Store Lua Store MHz MHz MQTT bridge Pont MQTT Map Carte @@ -977,8 +976,6 @@ tap to exit full screen toucher pour quitter le plein écran zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ampli ~17 dB pour zones calmes/isolées ; désactiver en zone bruyante. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +997,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1010,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1036,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1044,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1053,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1065,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1082,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/hu.lang b/deploy/apps/lang/hu.lang index 84dfac7..e1167b2 100644 --- a/deploy/apps/lang/hu.lang +++ b/deploy/apps/lang/hu.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: hu -# ver: 18 +# ver: 19 # name: Magyar # base: hu (device behind) (készülék hátulján) @@ -703,7 +703,7 @@ SD card remounted SD kártya újracsatolva SD card removed SD-kártya eltávolítva SD data is unavailable - contacts and channels cannot be saved until the card is reinserted. Az SD-kártya adatai nem érhetők el – a névjegyek és a csatornák nem menthetők a kártya újbóli behelyezéséig. SD data is unavailable - identity, settings, contacts and channels cannot be saved until the card is reinserted. Az SD-kártya adatai nem érhetők el – az azonosító, a beállítások, a névjegyek és a csatornák nem menthetők a kártya újbóli behelyezéséig. -SD data migration is incomplete. Identity and settings remain internal; use Copy internal data to SD to retry. Az SD-kártya adatainak áthelyezése nem teljes. Az azonosító és a beállítások továbbra is a belső tárhelyen vannak; a folytatáshoz használd a „Belső adatok másolása SD-kártyára” lehetőséget. +SD data migration is incomplete. Identity and settings remain internal; use Copy internal data to SD to retry. Az SD-kártya adatainak áthelyezése nem teljes. Az azonosító és a beállítások továbbra is a belső tárhelyen vannak; a folytatáshoz használd a "Belső adatok másolása SD-kártyára" lehetőséget. SD download failed: %s SD-letöltés sikertelen: %s SD formatted - %s (MESHCOMOD) SD formázás - %s (MESHCOMOD) SD is busy - close tools and retry Az SD-kártya foglalt – zárd be az eszközöket, majd próbáld újra. @@ -812,7 +812,6 @@ Sleep ready Készen áll az alvásra Slot %d cleared %d hely kiürítve Slot empty Üres hely Small\nMedium\nLarge Kicsi\nKözepes\nNagy -Snake Snake Snake — swipe or roll the trackball Kígyó – pöccintsd vagy görgesd a görgetőgolyót Sort Sort Sound Hang @@ -997,8 +996,6 @@ tap to exit full screen koppintson a teljes képernyős kilépéshez zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ~17 dB erősítővel csendes/távoli helyeken; zajos helyeken kapcsold ki. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope Hatókör another region másik régió my region saját régió @@ -1065,11 +1062,10 @@ sensors érzékelők Auto-add on for %s — new %s land in Contacts automatically. Automatikus hozzáadás bekapcsolva: %s — az új %s automatikusan a Névjegyek közé kerülnek. Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed under the Open Database License (ODbL).\n\n Térkép adatok © OpenStreetMap közreműködői.\nopenstreetmap.org/copyright\nAz Open Database License (ODbL) licenc alatt.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Térkép stílus © OpenTopoMap (CC-BY-SA) — opentopomap.org\nTérkép adatok © OpenStreetMap közreműködői (ODbL) + SRTM.\n\n -How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. A csempék működése:\nA térkép 256×256 képpontos „slippy” csempékből épül fel. Csak a megtekintett területhez szükséges csempék töltődnek le — nincs tömeges előzetes letöltés.\n\nMivel az eszköz nem támogatja a HTTPS-t (a Wi-Fi elindítása után nincs elegendő heap memória), és a JPEG képek dekódolása sokkal kevesebb erőforrást igényel, mint a PNG-é, a csempék a wadamesh proxyn keresztül érkeznek: a proxy HTTPS-en keresztül lekéri a forrásból a PNG-t egy azonosító User-Agent használatával, JPEG formátumba kódolja át, majd eltárolja a gyorsítótárban. Az eszköz ezután minden csempét a saját flash memóriájában is eltárol, így egy csempét csak egyszer kell letölteni.\n\nAz Options → Reload tiles lehetőséggel újra letöltheted a jelenleg megtekintett terület csempéit, ha valamelyik hibásan jelenik meg. +How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. A csempék működése:\nA térkép 256×256 képpontos "slippy" csempékből épül fel. Csak a megtekintett területhez szükséges csempék töltődnek le — nincs tömeges előzetes letöltés.\n\nMivel az eszköz nem támogatja a HTTPS-t (a Wi-Fi elindítása után nincs elegendő heap memória), és a JPEG képek dekódolása sokkal kevesebb erőforrást igényel, mint a PNG-é, a csempék a wadamesh proxyn keresztül érkeznek: a proxy HTTPS-en keresztül lekéri a forrásból a PNG-t egy azonosító User-Agent használatával, JPEG formátumba kódolja át, majd eltárolja a gyorsítótárban. Az eszköz ezután minden csempét a saját flash memóriájában is eltárol, így egy csempét csak egyszer kell letölteni.\n\nAz Options → Reload tiles lehetőséggel újra letöltheted a jelenleg megtekintett terület csempéit, ha valamelyik hibásan jelenik meg. "%s" wants to send discovery probes.\n\nEach probe asks every node in range to reply, so it uses airtime on the whole local mesh, not just yours.\n\nNothing is sent under your name and no message is transmitted. "%s" felfedezési probe-ok küldését kéri.\n\nMinden probe válaszra kéri a hatótávolságon belüli node-okat, ezért a teljes helyi mesh hálózat airtime-ját használja, nem csak a tiédet.\n\nSemmi sem kerül elküldésre a nevedben, és üzenet sem kerül továbbításra. ? ? Auth failed Auth failed -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Csak közvetlen (0-hop) @@ -1078,16 +1074,12 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Szöveg megadása Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... Link lost Link lost MESH MESH Name (A-Z) Név (A–Z) Nearest first Legközelebbi elöl -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Legutóbbi üzenet Reload tiles in view Csempék újratöltése Repeaters Repeaters @@ -1098,13 +1090,8 @@ Show contacts Névjegyek megjelenítése Show coordinates Koordináták Show tile z/x/y Csempe z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Csempe hiba keret -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite tap to cycle off / on / auto tap to cycle off / on / auto #7A7F87 Scanning… nothing has answered yet# #7A7F87 Scanning… nothing has answered yet# Export crash report (%uK) Hibajelentés exportálása (%uK) @@ -1116,3 +1103,23 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. A beta_%d telepítése?\nEz leminősíti a firmware-t és újraindul. +Other (hidden) network… Other (hidden) network… +Paused Paused +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal (stale) Jel (elavult) +nothing heard yet Még semmi hír. diff --git a/deploy/apps/lang/it.lang b/deploy/apps/lang/it.lang index e0350d1..af103b8 100644 --- a/deploy/apps/lang/it.lang +++ b/deploy/apps/lang/it.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: it -# ver: 18 +# ver: 19 # name: Italiano # base: it (device behind) (dispositivo indietro) @@ -452,7 +452,6 @@ Login OK (perms %u) Accesso OK (permessi %u) Login failed Accesso non riuscito Logs Log Longitude Longitudine -Lua Store Lua Store MHz MHz MQTT bridge Ponte MQTT Map Mappa @@ -977,8 +976,6 @@ tap to exit full screen tocca per uscire da schermo intero zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ampli ~17 dB per zone tranquille/remote; spegni in zone rumorose. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +997,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1010,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1036,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1044,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1053,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1065,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1082,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/nl.lang b/deploy/apps/lang/nl.lang index a7f4f93..b8a59f9 100644 --- a/deploy/apps/lang/nl.lang +++ b/deploy/apps/lang/nl.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: nl -# ver: 18 +# ver: 19 # name: Nederlands # base: nl (device behind) (apparaat loopt achter) @@ -452,7 +452,6 @@ Login OK (perms %u) Inloggen OK (rechten %u) Login failed Inloggen mislukt Logs Logboeken Longitude Lengtegraad -Lua Store Lua Store MHz MHz MQTT bridge MQTT-brug Map Kaart @@ -797,7 +796,6 @@ Sleep ready Klaar voor slaap Slot %d cleared Slot %d gewist Slot empty Slot leeg Small\nMedium\nLarge Klein\nMiddel\nGroot -Snake Snake Snake — swipe or roll the trackball Snake — veeg of rol de trackball Sort Sorteer Sound Geluid @@ -805,7 +803,6 @@ Sound off Geluid uit Sound on Geluid aan Sound: %s Geluid: %s Start time Starttijd -Status Status Step 1 of 3 Stap 1 van 3 Step 2 of 3 Stap 2 van 3 Step 3 of 3 Stap 3 van 3 @@ -977,8 +974,6 @@ tap to exit full screen tik om volledig scherm te verlaten zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. ~17 dB versterker voor stille/afgelegen gebieden; uit in drukke plekken. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +995,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1008,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1034,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1042,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1051,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1063,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1080,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/pt-br.lang b/deploy/apps/lang/pt-br.lang index e39cb21..96cda17 100644 --- a/deploy/apps/lang/pt-br.lang +++ b/deploy/apps/lang/pt-br.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: pt-br -# ver: 18 +# ver: 19 # name: Português (BR) # base: pt-br (device behind) (dispositivo atrasado) @@ -805,7 +805,6 @@ Sound off Som desligado Sound on Som ligado Sound: %s Som: %s Start time Hora de início -Status Status Step 1 of 3 Passo 1 de 3 Step 2 of 3 Passo 2 de 3 Step 3 of 3 Passo 3 de 3 @@ -977,8 +976,6 @@ tap to exit full screen toque para sair da tela cheia zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. amplificador ~17 dB para áreas silenciosas/remotas; desligue em locais ruidosos. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +997,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1010,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1036,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1044,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1053,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1065,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1082,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/ro.lang b/deploy/apps/lang/ro.lang index 6967b9d..0331e6c 100644 --- a/deploy/apps/lang/ro.lang +++ b/deploy/apps/lang/ro.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: ro -# ver: 18 +# ver: 19 # name: Română # base: ro (device behind) (dispozitiv în urmă) @@ -978,7 +978,6 @@ zoom zoom zoom %d zoom %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. amplificator ~17 dB pentru zone liniştite/izolate; opreşte-l în locuri zgomotoase. © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +999,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1012,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1038,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1046,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1055,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1067,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1084,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/ru.lang b/deploy/apps/lang/ru.lang index ceb1ec3..d8a02db 100644 --- a/deploy/apps/lang/ru.lang +++ b/deploy/apps/lang/ru.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: ru -# ver: 18 +# ver: 19 # name: Русский # base: ru (device behind) (устройство отстаёт) @@ -977,8 +977,6 @@ tap to exit full screen нажмите для выхода из полноэкр zoom масштаб zoom %d масштаб %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. усилитель ~17 дБ для тихих/удалённых мест; выключайте в шумных. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/sr.lang b/deploy/apps/lang/sr.lang index 0aab3e1..b1d9f31 100644 --- a/deploy/apps/lang/sr.lang +++ b/deploy/apps/lang/sr.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: sr -# ver: 18 +# ver: 19 # name: Српски # base: sr (device behind) (уређај касни) @@ -826,7 +826,7 @@ TCP on TCP укљ TOUCH BETA TOUCH BETA TX TX Tab hotkeys — tap a row, then press a key Пречице картице — додирни ред, па притисни тастер -Tap Use to switch - the device reboots to apply. Додирните „Користи“ да промените — уређај ће се поново покренути. +Tap Use to switch - the device reboots to apply. Додирните "Користи" да промените — уређај ће се поново покренути. Tap Use to switch - the device reboots to apply. Language files live in /lang on the storage; edit them or add your own. Додирни «Користи» за промену - уређај се поново покреће. Датотеке језика су у /lang на меморији; уреди их или додај своје. Tap a language; the device reboots to apply. Изаберите језик; уређај ће се рестартовати. Taste the rainbow! Окуси дугу! @@ -977,8 +977,6 @@ tap to exit full screen додирните за излаз из целог ек zoom зум zoom %d зум %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. појачало ~17 dB за тиха/удаљена места; искључи у бучним. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/lang/uk.lang b/deploy/apps/lang/uk.lang index 4ebd00f..4cba790 100644 --- a/deploy/apps/lang/uk.lang +++ b/deploy/apps/lang/uk.lang @@ -1,7 +1,7 @@ # wadamesh language file # canonical source - translators: edit this file and PR it # code: uk -# ver: 18 +# ver: 19 # name: Українська # base: uk (device behind) (пристрій відстає) @@ -977,8 +977,6 @@ tap to exit full screen торкніться, щоб вийти з повное zoom масштаб zoom %d масштаб %d ~17 dB amp for quiet/remote areas; turn off in noisy spots. підсилювач ~17 дБ для тихих/віддалених місць; вимикайте в галасливих. -© OpenStreetMap © OpenStreetMap -© OpenTopoMap © OpenTopoMap Scope another region my region @@ -1000,7 +998,6 @@ Overwrite oldest non-favorite 1-character messages will be shown 1-character messages will be shown All All Allow Allow -Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Apps that may send messages on the mesh. Anything they send goes out under your node name and cannot be told apart from a message you typed. Turn one off to take the permission back. Ignore 1-character messages Ignore 1-character messages Join w/ password Join w/ password Location no longer shared Location no longer shared @@ -1014,9 +1011,7 @@ Never\nChosen contacts only\nAnyone who asks Never\nChosen contacts only\nAnyone No apps installed. No apps installed. No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) No reply. Path hash is %u bytes;\nolder repeaters drop those.\nTry 1 byte (Settings > Radio & Mesh) Not supported on this board Not supported on this board -Read incoming messages Read incoming messages Reconnected to %.40s Reconnected to %.40s -Send messages as me Send messages as me Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Sent only when that contact asks, encrypted to them. Nothing is broadcast. Needs "Answer telemetry requests" on. Pick contacts in a contact's menu. Server has forgotten you.\nJoin with the room password. Server has forgotten you.\nJoin with the room password. Share my loc Share my loc @@ -1042,7 +1037,6 @@ App permissions App permissions About / credits About / credits Auth failed Auth failed Auto-add on for %s — new %s land in Contacts automatically. Auto-add on for %s — new %s land in Contacts automatically. -Batteryactivityon Batteryactivityon Console mode Console mode Console mode (experimental) Console mode (experimental) Direct (0-hop) only Direct (0-hop) only @@ -1051,8 +1045,6 @@ EXPERIMENTAL. Boots into a text console with no graphical interface: type comman Enter text Enter text Entering console mode - rebooting... Entering console mode - rebooting... Favorite Favorite -Geblokkeerde gebruikers Geblokkeerde gebruikers -Hide system filesShow system files Hide system filesShow system files How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. How tiles work:\nThe map is built from 256×256 "slippy" tiles. Only the tiles for the area you're viewing are fetched — there is no bulk pre-download.\n\nBecause this device can't do HTTPS (not enough heap after Wi-Fi starts) and decodes JPEG far more cheaply than PNG, tiles come from the wadamesh proxy: it fetches the upstream PNG over HTTPS with an identifying User-Agent, re-encodes it as JPEG, and caches it. Your device then caches each tile to its own flash, so a tile is only downloaded once.\n\nUse Options → Reload tiles to re-download the tiles currently in view if one looks corrupted. Init… Init… Leaving console mode - rebooting... Leaving console mode - rebooting... @@ -1062,8 +1054,6 @@ Map data © OpenStreetMap contributors.\nopenstreetmap.org/copyright\nLicensed u Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Map style © OpenTopoMap (CC-BY-SA) — opentopomap.org\nMap data © OpenStreetMap contributors (ODbL) + SRTM.\n\n Name (A-Z) Name (A-Z) Nearest first Nearest first -Other networksNetworks Other networksNetworks -Paste (move)Paste (copy) Paste (move)Paste (copy) Recent message Recent message Reload tiles in view Reload tiles in view Repeaters Repeaters @@ -1076,13 +1066,8 @@ Show contacts Show contacts Show coordinates Show coordinates Show tile z/x/y Show tile z/x/y Starting… Starting… -Stop sharing loc Share my loc Stop sharing loc Share my loc -Supprimer Supprimer -Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Switch to the external antenna?\n\nMake sure an antenna is actually connected to the external antenna socket first. Transmitting with nothing attached can damage the radio.\n\nResets to the on-board antenna on every reboot.Turn on legacy per-transmit switching?\n\nDiagnostic mode, for comparison only. It transmits on the on-board antenna and listens on the external one, so your outbound signal will be much weaker than your inbound.\n\nResets to the on-board antenna on every reboot. Text console (experimental) Text console (experimental) Tile debug overlay Tile debug overlay -Unblock Block Unblock Block -Unfav Favorite Unfav Favorite chats chats repeaters repeaters rooms rooms @@ -1098,3 +1083,26 @@ channel busy %.1f%% channel busy %.1f%% sampling... sampling... teal = now blue = average teal = now blue = average window restarted window restarted +Filter Filter +Hide system files Hide system files +Networks Networks +Other networks Other networks +Show system files Show system files +Unfav Unfav +Firmware beta_%d\nChecking for updates over Wi-Fi… Firmware beta_%d\nChecking for updates over Wi-Fi… +Install beta_%d?\nThis downgrades the firmware and reboots. Install beta_%d?\nThis downgrades the firmware and reboots. +Other (hidden) network… Other (hidden) network… +Paused Paused +Scan again Scan again +At a glance At a glance +At a glance disabled At a glance disabled +At a glance enabled At a glance enabled +At a glance while locked At a glance while locked +Glance only shows when unlocked Glance only shows when unlocked +Glance shows message previews\nwhile the screen is locked Glance shows message previews\nwhile the screen is locked +Snake — swipe, roll, or press a direction Snake — swipe, roll, or press a direction +N/A N/A +This board cannot run this app. This board cannot run this app. +Signal Signal +Signal (stale) Signal (stale) +nothing heard yet nothing heard yet diff --git a/deploy/apps/langs.json b/deploy/apps/langs.json index 76bba5e..7467d82 100644 --- a/deploy/apps/langs.json +++ b/deploy/apps/langs.json @@ -3,67 +3,67 @@ { "code": "bg", "name": "Български", - "ver": "18" + "ver": "19" }, { "code": "de", "name": "Deutsch", - "ver": "18" + "ver": "19" }, { "code": "el", "name": "Ελληνικά", - "ver": "18" + "ver": "19" }, { "code": "es", "name": "Español", - "ver": "18" + "ver": "19" }, { "code": "fr", "name": "Français", - "ver": "18" + "ver": "19" }, { "code": "hu", "name": "Magyar", - "ver": "18" + "ver": "19" }, { "code": "it", "name": "Italiano", - "ver": "18" + "ver": "19" }, { "code": "nl", "name": "Nederlands", - "ver": "18" + "ver": "19" }, { "code": "pt-br", "name": "Português (BR)", - "ver": "18" + "ver": "19" }, { "code": "ro", "name": "Română", - "ver": "18" + "ver": "19" }, { "code": "ru", "name": "Русский", - "ver": "18" + "ver": "19" }, { "code": "sr", "name": "Српски", - "ver": "18" + "ver": "19" }, { "code": "uk", "name": "Українська", - "ver": "18" + "ver": "19" } ] } diff --git a/deploy/apps/sdktest/1.7/sdktest.json b/deploy/apps/sdktest/1.7/sdktest.json new file mode 100644 index 0000000..aa4e24e --- /dev/null +++ b/deploy/apps/sdktest/1.7/sdktest.json @@ -0,0 +1 @@ +{"id":"sdktest","name":"SDK Test","ver":"1.7","desc":"Developer tool. Checks the extended Lua SDK on this board: capabilities, clock, battery, GPS, private files, audio API, read-only SD listing, crypto vectors, packet identity, channels, and opt-in transmit calls."} diff --git a/deploy/apps/sdktest/1.7/sdktest.lua b/deploy/apps/sdktest/1.7/sdktest.lua new file mode 100644 index 0000000..94b64da --- /dev/null +++ b/deploy/apps/sdktest/1.7/sdktest.lua @@ -0,0 +1,293 @@ +-- SDK self-test. Exercises the extended SDK so the results can be read off the +-- screen instead of inferred from a build log. Published to the store as a +-- developer/bench tool. +-- +-- 1.2 adds wada.crypto (checked against published RFC vectors, so a PASS here is +-- real evidence and not just "it returned something"), wada.mesh.channels, and +-- wada.mesh.send_dm. +-- 1.3 adds the discovery surface (wada.mesh.discover / discovered), packet +-- identity in rx_log, exact micro-degree coordinates, wada.ui.input and the +-- windowed wada.fs.read. +-- 1.6 adds wada.sd.list: capability reporting, traversal rejection, root +-- metadata, and a nested directory listing when the card contains one. +-- 1.7 adds storage-neutral wada.audio capability and API-shape checks. Playback +-- stays manual so opening the bench test never emits sound unexpectedly. +-- 1.5 uses wada.ui.text_lines() to measure instead of estimating, where the +-- firmware has it. That call was added because of the 1.3 bug below: an app +-- could ask how tall a line is but not how wide, so laying out rows meant +-- guessing whether text would wrap. Falls back to the 1.4 estimate on older +-- firmware, so it still lays out correctly there. +-- 1.4 fixes rows drawing on top of one another. label:width() turns wrapping +-- on, so any line wider than the screen became two lines while the caller still +-- advanced by one, and the next row landed on top. Reported with a photo of the +-- channels line sitting across the contacts line. row() now owns the cursor and +-- advances by the height the text actually needs. +local ui, sys, store, timer = wada.ui, wada.sys, wada.store, wada.timer +local C = ui.colors + +local app = {} +local rows, keyline, sendline, dmline, msgline = {}, nil, nil, nil, nil +local discline, inputline = nil, nil +local W = 300 + +-- row() owns the vertical cursor. Every caller used to pass a y and then add a +-- hand-picked constant, which is only correct while the text fits on one line. +-- An app cannot measure rendered text, so the wrapped line count is estimated +-- from a deliberately pessimistic character width: over-estimating costs a +-- little whitespace, under-estimating overlaps the next row. +local LH, GAP = 14, 3 +local cy = 4 +local function row(text, color, extra_gap) + local l = ui.label(text, 6, cy, 12, color or C.text) + l:width(W - 12) + rows[#rows + 1] = l + local n + if ui.text_lines then + n = ui.text_lines(tostring(text), W - 12, 12) -- measured: exact + else + local cpl = math.max(16, (W - 12) // 7) -- older firmware: estimate + n = 0 + for seg in (tostring(text) .. "\n"):gmatch("(.-)\n") do + n = n + math.max(1, math.ceil(#seg / cpl)) + end + end + cy = cy + math.max(1, n or 1) * LH + GAP + (extra_gap or 0) + return l +end +local function yn(v) return v and "yes" or "NO" end + +function app.on_open(w, h) + W = w or 300 + ui.scroll(true) + cy = 4 + local y + + local c = sys.caps() + row("caps: sdk_ext=" .. yn(c.sdk_ext) .. " kbd=" .. yn(c.keyboard) .. + " touch=" .. yn(c.touch) .. " sd=" .. yn(c.sd), C.accent) + row("caps: sd_list=" .. yn(c.sd_list) .. " discover=" .. yn(c.discover) .. + " input=" .. yn(c.input) .. + " rx_identity=" .. yn(c.rx_identity), C.accent) + row("caps: audio=" .. yn(c.audio) .. " wav=" .. yn(c.audio_wav) .. + " mp3=" .. yn(c.audio_mp3) .. " audio_sd=" .. yn(c.audio_sd), C.accent) + if c.audio then + local api_ok = wada.audio and type(wada.audio.play) == "function" and + type(wada.audio.pause) == "function" and + type(wada.audio.resume) == "function" and + type(wada.audio.stop) == "function" and + type(wada.audio.status) == "function" + local status = api_ok and wada.audio.status() or nil + api_ok = api_ok and type(status) == "table" and type(status.state) == "string" + row("wada.audio API: " .. (api_ok and ("PASS (" .. status.state .. ")") or "FAIL"), + api_ok and C.good or C.bad) + else + row("wada.audio: unavailable on this board", C.sub) + end + row("layout: " .. (ui.text_lines and "measured (ui.text_lines)" or "estimated (older firmware)"), + ui.text_lines and C.good or C.sub) + -- crypto: published test vectors, so this is checkable rather than merely alive + if wada.crypto then + local hex = wada.crypto.hex + local k20 = string.rep(string.char(0x0b), 20) + local checks = { + { "sha256('abc')", hex(wada.crypto.sha256("abc")), + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" }, + { "sha1('abc')", hex(wada.crypto.sha1("abc")), + "a9993e364706816aba3e25717850c26c9cd0d89d" }, + { "hmac_sha1 RFC2202#1", hex(wada.crypto.hmac_sha1(k20, "Hi There")), + "b617318655057264e28bc0b6fb378c8ef146be00" }, + { "hmac_sha256 RFC4231#1", hex(wada.crypto.hmac_sha256(k20, "Hi There")), + "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7" }, + } + local allok = true + for _, t in ipairs(checks) do + local ok = (t[2] == t[3]) + if not ok then allok = false end + row("crypto " .. t[1] .. ": " .. (ok and "PASS" or ("FAIL got " .. tostring(t[2]):sub(1, 16))), + ok and C.good or C.bad) + end + -- The point of having it in C: this loop would blow the instruction budget in Lua. + local t0 = sys.millis() + for _ = 1, 200 do wada.crypto.hmac_sha1(k20, "Hi There") end + row(string.format("crypto: 200 x hmac_sha1 in %d ms%s", sys.millis() - t0, + allok and "" or " (VECTORS FAILED)"), allok and C.good or C.bad) + else + row("wada.crypto: MISSING", C.bad) + end + + -- channel discovery + local chans = wada.mesh.channels and wada.mesh.channels() or nil + if chans then + local names = table.concat(chans, ", ") + row("channels(): " .. #chans .. " " .. names:sub(1, 60), C.text) + else + row("wada.mesh.channels: MISSING", C.bad) + end + + if not c.sdk_ext then + row("extended SDK is OFF on this board - stopping here.", C.sub) + return + end + + -- contacts, so send_dm has a target to name. 1.3 also checks the pubkey field. + local cts = wada.mesh.contacts() + local first = cts[1] and cts[1].name or nil + row("contacts: " .. #cts .. (first and (" first=" .. first) or ""), C.text) + if cts[1] then + local pk = cts[1].pubkey + row("contacts[1].pubkey: " .. tostring(pk) .. + (type(pk) == "string" and #pk == 8 and " PASS" or " FAIL"), + (type(pk) == "string" and #pk == 8) and C.good or C.bad) + end + + local me = wada.mesh.self() + row("self: " .. tostring(me.name) .. " pubkey=" .. tostring(me.pubkey), C.text) + -- Exact coordinates. Lua's floats here are 32-bit, so an app that logs a track + -- must use the _e6 integers; this proves they are present and consistent. + local fix = sys.gps() + if fix then + local drift = math.abs(fix.lat_e6 / 1e6 - fix.lat) + row(string.format("gps: %d,%d e6 alt %dm %d sats drift %.6f %s", + fix.lat_e6, fix.lon_e6, fix.alt_m or 0, fix.sats, drift, + drift < 0.001 and "PASS" or "FAIL"), drift < 0.001 and C.good or C.bad) + else + row("gps: no fix (normal indoors)", C.sub) + end + + -- rx_log identity: adverts carry a real public key, addressed frames carry + -- one-byte hashes, everything else carries nothing. All three are correct. + local log = wada.mesh.rx_log() + local withpk, withsrc = 0, 0 + for _, r in ipairs(log) do + if r.pubkey then withpk = withpk + 1 end + if r.src then withsrc = withsrc + 1 end + end + row(string.format("rx_log: %d frames, %d with a pubkey (adverts), %d with src/dst", + #log, withpk, withsrc), #log > 0 and C.text or C.sub) + -- fs: windowed read. Writes once (the 1/sec limit means one write per open). + if wada.fs then + local probe = "0123456789abcdef" + wada.fs.write("sdktest.bin", probe) + local part, total = wada.fs.read("sdktest.bin", 4, 4) + local ok = (part == "4567" and total == #probe) + row("fs.read(name,4,4): " .. tostring(part) .. " total=" .. tostring(total) .. + (ok and " PASS" or " FAIL"), ok and C.good or C.bad) + end + + -- Physical SD listing is read-only and independently feature-detected. A + -- supported board with no inserted card is a normal bench state, not a test + -- failure; API shape and path rejection can still be checked separately. + if c.sd_list then + if not wada.sd or not wada.sd.list then + row("wada.sd.list: MISSING despite caps", C.bad) + else + local guard_ok, guard_failure = true, nil + local bad_paths = { "/..", "/.", "/a/../b", "/a//b", "/a/", "relative", "/a\\b" } + for _, path in ipairs(bad_paths) do + local rejected, baderr = wada.sd.list(path) + if rejected ~= nil or baderr ~= "bad path" then + guard_ok = false + guard_failure = path .. " -> " .. tostring(baderr) + break + end + end + row("sd traversal guard: " .. (guard_ok and "PASS" or ("FAIL " .. guard_failure)), + guard_ok and C.good or C.bad) + + local entries, err = wada.sd.list("/") + if not entries then + local note = err == "busy" and " (retry later)" or " (no readable card)" + row("sd.list('/'): " .. tostring(err) .. note, C.sub) + else + local metadata_ok, first_dir = true, nil + for _, entry in ipairs(entries) do + if type(entry.name) ~= "string" or + (entry.type ~= "file" and entry.type ~= "dir") or + type(entry.size) ~= "number" or type(entry.mtime) ~= "number" then + metadata_ok = false + end + if not first_dir and entry.type == "dir" then first_dir = entry.name end + end + row("sd.list('/'): " .. #entries .. " entries, metadata " .. + (metadata_ok and "PASS" or "FAIL") .. + (entries.truncated and " (truncated)" or ""), + metadata_ok and C.good or C.bad) + if first_dir then + local nested, nested_err = wada.sd.list("/" .. first_dir) + row("sd.list('/" .. first_dir .. "'): " .. + (nested and (#nested .. " entries PASS") or ("FAIL " .. tostring(nested_err))), + nested and C.good or C.bad) + else + row("sd nested listing: no directory on card to test", C.sub) + end + end + end + else + row("wada.sd.list: unavailable on this board", C.sub) + end + + sendline = row("mesh.send: not tried yet", C.sub) + dmline = row("mesh.send_dm: not tried yet", C.sub) + msgline = row("on_message: waiting (needs the read permissions)", C.sub) + y = cy + 4 + ui.button("Send to Public", 6, y, 120, 30, function() + local ok, err = wada.mesh.send("Public", "wadamesh SDK self-test") + sendline:set("mesh.send: " .. tostring(ok) .. " " .. tostring(err)) + sendline:color(ok and C.good or C.bad) + end) + ui.button("DM first contact", 132, y, 130, 30, function() + if not first then dmline:set("mesh.send_dm: no contacts to target"); dmline:color(C.bad); return end + local ok, err = wada.mesh.send_dm(first, "wadamesh SDK self-test (DM)") + dmline:set("mesh.send_dm -> " .. first .. ": " .. tostring(ok) .. " " .. tostring(err)) + dmline:color(ok and C.good or C.bad) + end) + cy = y + 38 + discline = row("mesh.discover: not tried yet", C.sub) + inputline = row("ui.input: not tried yet", C.sub) + -- Probing TRANSMITS and makes every neighbour reply, so it is a button, not + -- something this app does on open. + y = cy + 4 + ui.button("Probe", 6, y, 90, 30, function() + local tag, err = wada.mesh.discover() + if not tag then + discline:set("mesh.discover: " .. tostring(err)); discline:color(C.bad); return + end + discline:set("mesh.discover: sent, waiting for replies..."); discline:color(C.text) + end) + ui.button("Results", 102, y, 90, 30, function() + local hits = wada.mesh.discovered() + if #hits == 0 then + discline:set("discovered(): nothing yet - probe, then wait a few seconds") + discline:color(C.sub); return + end + local h = hits[1] + discline:set(string.format("discovered(): %d first %s snr %.1f/%.1f %s", + #hits, h.name or h.pubkey, h.snr, h.their_snr, h.direct and "direct" or (h.hops .. "h"))) + discline:color(C.good) + end) + y = y + 34 + ui.button("Beep", 6, y, 70, 30, function() sys.beep() end) + ui.button("Input", 82, y, 90, 30, function() + ui.input("Type anything", "hello", function(text) + inputline:set("ui.input -> " .. (text and ("'" .. text .. "'") or "cancelled")) + inputline:color(text and C.good or C.sub) + end) + end) +end + +function app.on_input(ev) + if ev.type == "key" and keyline then + keyline:set("keys: got '" .. tostring(ev.key) .. "'") + keyline:color(C.good) + end +end + +-- Proves the kind field and that DMs/rooms reach an app at all, not just channels. +function app.on_message(m) + if not msgline then return end + msgline:set("on_message: kind=" .. tostring(m.kind) .. " from=" .. tostring(m.sender) .. + " ch=" .. tostring(m.channel) .. " text=" .. tostring(m.text):sub(1, 20)) + msgline:color(C.good) +end + +return app diff --git a/deploy/apps/wardrive/1.1/wardrive.json b/deploy/apps/wardrive/1.1/wardrive.json new file mode 100644 index 0000000..d467e18 --- /dev/null +++ b/deploy/apps/wardrive/1.1/wardrive.json @@ -0,0 +1 @@ +{"id":"wardrive","name":"Wardrive","ver":"1.1","desc":"LoRa coverage survey. Probes every 20s, logs each reply with GPS position, altitude and BOTH link directions to a CSV you can pull off the device. Reference app for wada.mesh.discover."} \ No newline at end of file diff --git a/deploy/apps/wardrive/1.1/wardrive.lua b/deploy/apps/wardrive/1.1/wardrive.lua new file mode 100644 index 0000000..a2f8377 --- /dev/null +++ b/deploy/apps/wardrive/1.1/wardrive.lua @@ -0,0 +1,216 @@ +-- Wardrive (Lua) — a LoRa coverage survey that logs to a CSV you can pull off +-- the device afterwards. +-- +-- Reference app for the discovery half of the SDK. It works the way a survey +-- has to work: it PROBES rather than listens. A probe is a zero-hop request +-- that every node in earshot answers, so a reply proves the link works from +-- exactly where you are standing. Listening only ever tells you what happened +-- to transmit while you were there, which is a different and much weaker claim. +-- +-- Each reply carries both directions of the link: how well we heard them, and +-- how well they heard us. They are rarely equal, and the asymmetry is the point +-- -- "I can hear the repeater but it cannot hear me" is not the same fact as +-- "no coverage", and only a probe reveals it. +local ui, sys, mesh, fs, timer = wada.ui, wada.sys, wada.mesh, wada.fs, wada.timer +local C = ui.colors +local app = {} + +local SWEEP_MS = 20000 -- above the 15 s floor the firmware enforces on probes +local HARVEST_MS = 4000 -- replies land over the few seconds after a probe +local TYPE = { [1]="chat", [2]="repeater", [3]="room", [4]="sensor" } + +local run, running, samples, sweeps, last_err = "run", false, 0, 0, nil +local node_count = 0 +local phase, phase_at = "idle", 0 +local hdr, gps_lbl, stat_lbl, rows = nil, nil, nil, {} +local nodes = {} -- pubkey -> { name, type, best, worst, seen } +local pending = {} -- lines waiting on the 1 write/sec limit + +local function logname() return run .. ".csv" end + +-- Return at most max_bytes without splitting a UTF-8 sequence. The row's +-- fixed-width columns are byte-budgeted, not character-budgeted; a raw +-- string.sub(1, 14) cut Ouderkerk + sun + VS16 inside the final codepoint and +-- left LVGL unable to advance through the label (#323, same class as #223). +local function utf8_prefix_bytes(text, max_bytes) + local offset, last, length = 1, 0, #text + while offset <= length and offset <= max_bytes do + local first = text:byte(offset) + local width = first <= 0x7F and 1 + or (first >= 0xC2 and first <= 0xDF and 2) + or (first >= 0xE0 and first <= 0xEF and 3) + or (first >= 0xF0 and first <= 0xF4 and 4) or 0 + if width == 0 or offset + width - 1 > length or offset + width - 1 > max_bytes then break end + local valid = true + for i = 2, width do + local byte = text:byte(offset + i - 1) + if byte < 0x80 or byte > 0xBF then valid = false; break end + end + local second = width > 1 and text:byte(offset + 1) or 0 + if (first == 0xE0 and second < 0xA0) or (first == 0xED and second > 0x9F) or + (first == 0xF0 and second < 0x90) or (first == 0xF4 and second > 0x8F) then valid = false end + if not valid then break end + last = offset + width - 1 + offset = last + 1 + end + return text:sub(1, last) +end + +-- Lua here is built with 32-bit floats, so fix.lat is good to about a metre and +-- no better. fix.lat_e6 is the same reading as an exact integer in +-- micro-degrees, which is what belongs in a log: a survey you plot months later +-- should not carry rounding the device never had. +local HEADER = "epoch,lat_e6,lon_e6,alt_m,pubkey,name,type,rssi,snr,their_snr,hops" +local wrote_header = false + +-- The filesystem allows one write a second. Sweeps produce a burst of rows, so +-- they queue here and drain a chunk per tick instead of being dropped. +local function flush() + if #pending == 0 then return end + local chunk = table.concat(pending, "\n") .. "\n" + if not wrote_header then chunk = HEADER .. "\n" .. chunk end + local ok = fs.append(logname(), chunk) + if ok then pending, wrote_header = {}, true end +end + +local function record(fix, hit) + local key = hit.pubkey + local n = nodes[key] + if not n then + n = { name = hit.name or key, type = hit.type, best = hit.snr, worst = hit.snr, seen = 0 } + nodes[key] = n + node_count = node_count + 1 + end + if hit.snr > n.best then n.best = hit.snr end + if hit.snr < n.worst then n.worst = hit.snr end + n.seen = n.seen + 1 + if hit.name then n.name = hit.name end + + samples = samples + 1 + pending[#pending + 1] = string.format("%d,%d,%d,%d,%s,%s,%d,%d,%.2f,%.2f,%d", + fix.time or sys.epoch(), fix.lat_e6, fix.lon_e6, fix.alt_m or 0, + key, (hit.name or ""):gsub(",", " "), hit.type, + hit.rssi, hit.snr, hit.their_snr, hit.hops) +end + +local function sweep() + local tag, err = mesh.discover() -- every node type + if not tag then last_err = err; return false end + last_err = nil + sweeps = sweeps + 1 + return true +end + +local function harvest() + local fix = sys.gps() + if not fix then + -- No fix means the sample cannot be placed, so it is discarded rather than + -- logged at 0,0. A survey file with phantom points at Null Island is worse + -- than a shorter one. + mesh.discover_clear() + return + end + for _, hit in ipairs(mesh.discovered()) do record(fix, hit) end + mesh.discover_clear() -- next sample must not inherit this one +end + +local function redraw() + local fix = sys.gps() + if fix then + gps_lbl:set(string.format("%.5f, %.5f %dm %d sats", fix.lat, fix.lon, fix.alt_m or 0, fix.sats)) + gps_lbl:color(C.good) + else + gps_lbl:set("waiting for a GPS fix - samples are discarded until then") + gps_lbl:color(C.bad) + end + + local state = running and (phase == "probe" and "listening..." or "sweeping") or "stopped" + stat_lbl:set(string.format("%s | %s | %d sweeps, %d samples, %d nodes%s", + run, state, sweeps, samples, node_count, + last_err and (" [" .. last_err .. "]") or "")) + stat_lbl:color(last_err and C.bad or C.accent) + + local list = {} + for key, n in pairs(nodes) do list[#list + 1] = { key = key, n = n } end + table.sort(list, function(a, b) return a.n.best > b.n.best end) + for i = 1, #rows do + local e = list[i] + if e then + rows[i]:set(string.format("%-14s %-8s best %5.1f worst %5.1f x%d", + utf8_prefix_bytes(e.n.name, 14), TYPE[e.n.type] or "?", e.n.best, e.n.worst, e.n.seen)) + rows[i]:color(e.n.best > 0 and C.good or C.text) + else + rows[i]:set(i == 1 and "nothing has answered a probe yet" or "") + rows[i]:color(C.sub) + end + end +end + +function app.on_open(w, h) + if not sys.caps().discover then + ui.label("This board does not carry the extended SDK,", 6, 8, 12, C.bad) + ui.label("so it cannot send discovery probes.", 6, 26, 12, C.bad) + return + end + ui.scroll(true) + local LH = ui.text_h(12) + local y = 4 + + hdr = ui.label("LoRa coverage survey", 4, y, 12, C.accent); hdr:width(w - 10); y = y + LH + 3 + gps_lbl = ui.label("", 4, y, 12, C.sub); gps_lbl:width(w - 10); y = y + LH + 3 + stat_lbl = ui.label("", 4, y, 12, C.text); stat_lbl:width(w - 10); y = y + LH * 2 + 5 + + local bw = math.min(96, (w - 20) // 3) + ui.button("Start", 4, y, bw, 32, function() + running = not running + if running then phase, phase_at = "idle", 0 end + sys.toast(running and "Survey running" or "Survey stopped", 1200) + end) + ui.button("Name", 8 + bw, y, bw, 32, function() + ui.input("Name this run", run, function(text) + if text then + run = text:gsub("[^%w%-_]", "_") + wrote_header = false -- a new file needs its own header row + sys.toast("Logging to " .. logname(), 1500) + end + end) + end) + ui.button("Reset", 12 + bw * 2, y, bw, 32, function() + nodes, samples, sweeps, pending, node_count = {}, 0, 0, {}, 0 + mesh.discover_clear() + fs.remove(logname()) + wrote_header = false + sys.toast("Cleared " .. logname(), 1200) + end) + y = y + 38 + + ui.label("strongest first", 4, y, 12, C.sub); y = y + LH + 2 + for i = 1, 12 do + rows[i] = ui.label("", 4, y, 12, C.text); rows[i]:width(w - 10); y = y + LH + 2 + end + + redraw() + timer.every(1000) +end + +function app.on_tick() + if running then + local now = sys.millis() + if phase == "idle" or (phase == "wait" and now - phase_at >= SWEEP_MS) then + -- A refused or rate-limited probe backs off a full sweep interval. Retrying + -- every tick would re-enter the permission path once a second for nothing. + phase, phase_at = sweep() and "probe" or "wait", now + elseif phase == "probe" and now - phase_at >= HARVEST_MS then + harvest() + phase = "wait" -- phase_at stays at the probe time, so sweeps stay on cadence + end + end + flush() + redraw() +end + +function app.on_close() + flush() -- one last drain; anything queued would otherwise be lost +end + +return app diff --git a/deploy/site/sdk.html b/deploy/site/sdk.html index 6e66ce6..0defa3b 100644 --- a/deploy/site/sdk.html +++ b/deploy/site/sdk.html @@ -141,6 +141,7 @@ wada.net wada.store wada.fs + wada.audio wada.sd wada.sys wada.timer @@ -237,6 +238,7 @@ return app wada.ui.button(text, x, y, w, h, fn)A tappable button at x, y sized w × h; fn is called with no arguments. wada.ui.text_w(text [, size])Rendered width of text in pixels, for the given size class. wada.ui.text_lines(text, width [, size])How many lines text wraps to inside width. Counts explicit newlines too. Use this when you lay out your own rows. label:width() turns wrapping on, so a line longer than the screen becomes two while a fixed y step still advances by one, and the next row is drawn on top of it. This is the single most common way an app's layout goes wrong; it happened to the SDK Test app's channel list. + wada.ui.clear()Remove every widget from the app's page, leaving the page itself. This is how you build an app with more than one screen: clear, then create the next screen's widgets in the same place you built the first. Any handle you still hold from before the clear is dead — calling a method on it does nothing rather than crashing, but it will not come back, so re-create and re-assign. Older firmware has no ui.clear: write local clear = ui.clear or function() end if you need to run on both. wada.ui.list(x, y, w, h)A scrollable list of selectable rows — the "pick one of N" widget. Methods: add(text [, fn]) returning the row index, set(i, text), color(i, c), select(i) (highlights and scrolls it into view), selected(), count(), clear(), pos(x, y). Rows are real buttons, so keyboard and trackball navigation walks them on touchless boards without you doing anything. wada.ui.input(title, initial, cb)A modal text field. cb(text) on OK, cb(nil) on cancel or on the dialog being closed any other way — exactly one call, always, so an app that disabled itself while waiting always gets to re-enable. Uses the firmware's own dialog and keyboard, so it behaves identically on a touchscreen, on the Tanmatsu's physical keyboard and on a trackball board. One prompt at a time. wada.ui.canvas(w, h)A drawing surface. See the canvas methods below. @@ -291,7 +293,8 @@ return app

wada.mesh

- + + @@ -409,7 +412,7 @@ end
wada.mesh.contacts()Array of {name, pubkey, type, ago_s, lat, lon, lat_e6, lon_e6}. pubkey is the first 4 bytes as 8 hex characters — the same short form the rest of the interface uses, and what lines a contact up with a discovery hit.
wada.mesh.contacts([offset], [limit])Array of {name, pubkey, type, ago_s, lat, lon, lat_e6, lon_e6}. pubkey is the first 4 bytes as 8 hex characters — the same short form the rest of the interface uses, and what lines a contact up with a discovery hit. Returns at most limit entries (default 100, maximum 250) starting at offset, because every entry is an eight-field table and building thousands in one call would exhaust the app heap. On a device with more contacts than that, page through them: ask for 100, then 100 from offset 100, and so on.
wada.mesh.contact_count()How many contacts the device holds, so you can page through contacts() without calling it repeatedly to find where the list ends.
wada.mesh.rx_log()Recent packets, newest first: {ago_ms, type, rssi, snr, hops, route, len}, plus whatever identity the frame actually carried — see below. The RF Monitor feed.
wada.mesh.stats(){rssi, noise, rx_air_s, tx_air_s, rx_pkts, rx_err, tx_budget_ms, rx_events, rx_dropped, tx_pkts, freq, bw, sf, duty_pct}.
wada.mesh.self()This node: {name, pubkey, lat, lon, lat_e6, lon_e6}.

http:// only, both of them. On-device TLS is not workable at the heap these boards have left once Wi-Fi has associated, so there is no https to offer; put a proxy in front if you need it. One request in flight at a time per app.

The fetch is asynchronous and runs on the firmware's existing network worker, so it does not block the UI. Your callback runs on the UI thread once the body is in memory.

-

Plain HTTP only, and that is not an oversight. After Wi-Fi associates there is not enough free internal memory on the smaller boards for a TLS handshake — mbedTLS wants around 30 KB and roughly 5 KB is free. If you need an HTTPS source, put a small proxy in front of it, the way the map tiles do. Responses are capped (64 KB by default).

+

Plain HTTP only, and that is not an oversight. After Wi-Fi associates there is not enough free internal memory on the smaller boards for a TLS handshake — mbedTLS wants around 30 KB and roughly 5 KB is free. If you need an HTTPS source, put a small proxy in front of it, the way the map tiles do. Responses are capped (192 KB by default).

@@ -441,6 +444,24 @@ end +
+

wada.audio audio

+
+

Asynchronous audio playback from the current app's private storage. A plain name such as track.wav resolves inside the same app folder as wada.fs, whether that folder lives on internal flash, SD, or SD_MMC. Check wada.sys.caps().audio and audio_wav before using it.

+ + + + + + + +
CallReturns / does
wada.audio.play(name)Starts or replaces playback and returns true, or nil, error. Plain names use app-private storage. On boards with caps().audio_sd, sd:/Music/track.wav reads directly from the physical card.
wada.audio.pause()Pauses an active track. Returns whether the command was accepted.
wada.audio.resume()Resumes a paused track. Returns whether the command was accepted.
wada.audio.stop()Stops the active track. Returns whether the command was accepted.
wada.audio.status(){state, path, source, format, error}. State is stopped, playing, paused, ended, or error; error is present only after a playback failure.
+

Supported formats are PCM WAV (16-bit mono or stereo at 8–48 kHz) and MPEG Layer III MP3, including CBR, VBR, and ID3-tagged files. Stereo is mixed to mono on the device. Check audio_wav or audio_mp3 rather than assuming a format exists on older firmware.

+

Playback follows the user's Sound switch and volume, never blocks the Lua callback, and stops automatically when the app closes. A second play() replaces the current track. Playlist ordering belongs to the app, so there are no ambiguous host-side next() or previous() calls.

+

Plain names follow the same sandbox rules as wada.fs. Explicit sd: paths follow wada.sd path validation. Expected errors include bad path, no storage, no sd, not found, busy, muted, and unsupported format.

+
+
+

wada.sd sd_list

@@ -466,7 +487,7 @@ end wada.sys.datetime(){year, month, day, hour, min, sec, wday} in local time. wday is 0 for Sunday. wada.sys.tr(s)Translate s through the device's active language, using the same table the firmware's own interface uses. Returns s unchanged when there is no translation, so it is always safe to wrap a string. Add your keys to deploy/apps/lang/<code>.lang alongside the firmware's. Older firmware has no sys.tr: write local tr = sys.tr or function(x) return x end and call tr(). wada.sys.beep()Short beep on boards with a buzzer; silent elsewhere, and silent when the user has sound off. - wada.sys.caps(){sdk_ext, keyboard, touch, sd, sd_list, compass, accel, discover, input, rx_identity, list, packets, sensors, map, measure}. Check sdk_ext before using anything marked ext below, and sd_list before using wada.sd. The rest are feature flags for calls added after the extended SDK first shipped, so an app can degrade on older firmware instead of erroring. + wada.sys.caps(){sdk_ext, keyboard, touch, sd, sd_list, audio, audio_wav, audio_mp3, audio_sd, compass, accel, discover, input, rx_identity, list, packets, sensors, map, measure}. Check sdk_ext before using anything marked ext, sd_list before using wada.sd, and audio plus the format flag before using wada.audio. audio_sd means direct sd: paths are available; plain audio names use app storage and do not require it. wada.sys.battery() ext{mv, pct, charging}. wada.sys.env() sensors{temp_c, humidity, pressure_hpa, alt_m}, or nil. A field is present only when the hardware actually reported it, so you can tell "no humidity sensor" from "0% humidity". Gated on caps().sensors — does this board have the sensor rail — and not on sdk_ext, which is a memory gate. Those are different questions: the plain Heltec V4 has the Expansion Kit but not the extended SDK, and most boards with the extended SDK have no sensors at all. wada.sys.gps() ext{lat, lon, lat_e6, lon_e6, sats, alt_m, time, speed_kmh, course}, or nil with no fix — which is the normal indoor case, so handle it. lat_e6/lon_e6 are exact micro-degrees; lat/lon are single-precision floats, so log the integers. alt_m is metres. time is satellite time, absent until the receiver has decoded the date. speed_kmh and course (degrees clockwise from north) appear only on boards whose GPS provider reports them, and course only while actually moving — a stationary receiver has no course, so it is absent rather than 0. Treat those last three as optional. @@ -587,7 +608,7 @@ end

Apps run in a restricted environment. These are removed: io, os, require, dofile, and loading new chunks at runtime. Available: math, string, table, and the usual pairs, ipairs, select, pcall, tostring, tonumber.

Memory comes from a capped pool in PSRAM, so an app that allocates without bound fails its own allocation rather than starving the radio or the UI. There is an instruction budget on every callback for the same reason.

-

The extended calls are not on every board. Anything marked ext above — wada.fs, wada.mesh.send, wada.mesh.send_dm, wada.mesh.discover, sys.battery, sys.gps — needs a board with the memory to carry it, so the Heltec V4 keeps its RAM for the mesh instead. Call wada.sys.caps().sdk_ext and degrade gracefully rather than assuming. wada.sd additionally needs a physical card interface, reported by caps().sd_list.

+

The extended calls are not on every board. Anything marked ext above — wada.fs, wada.mesh.send, wada.mesh.send_dm, wada.mesh.discover, sys.battery, sys.gps — needs a board with the memory to carry it, so the Heltec V4 keeps its RAM for the mesh instead. Call wada.sys.caps().sdk_ext and degrade gracefully rather than assuming. wada.sd additionally needs a physical card interface, reported by caps().sd_list. wada.audio instead follows caps().audio: storage may be internal, but the device still needs a stream-capable speaker path.

Rate limits are part of the contract, not a rainy-day guard: wada.fs writes are about one per second with a 32 KB file cap, and wada.mesh.send has a 5 second floor and a 180-character limit. They return false, "too fast" rather than throwing, and hitting them is expected — handle it.

None of this makes a hostile app safe, which is why the catalog is curated. It makes an honest app that has a bug survivable: it gets closed, and the device keeps carrying traffic.

diff --git a/include/lv_conf.h b/include/lv_conf.h index 4c330f7..8808621 100644 --- a/include/lv_conf.h +++ b/include/lv_conf.h @@ -61,6 +61,13 @@ #define LV_FONT_MONTSERRAT_20 1 #define LV_FONT_MONTSERRAT_24 1 #endif +/* T-Deck-only, unrelated to the Large/Huge UI-scale block above: an experiment + * to shrink the "at a glance" notification's message body from 28px to 20px + * on this board only (see atGlanceEnsureFont() in UITask.cpp). Easy to revert + * by dropping this block + the T-Deck branch in atGlanceEnsureFont(). */ +#if defined(HAS_TDECK_GT911) +#define LV_FONT_MONTSERRAT_20 1 +#endif /* 28 px Montserrat for the boot splash title — keeps the rest of the UI on * the smaller fonts so the .data cost stays modest. */ #define LV_FONT_MONTSERRAT_28 1 diff --git a/include/lv_conf_m9.h b/include/lv_conf_m9.h new file mode 100644 index 0000000..db9237a --- /dev/null +++ b/include/lv_conf_m9.h @@ -0,0 +1,25 @@ +#pragma once + +// M9-specific LVGL config wrapper. +// +// Why this exists: +// - LVGL 8.4's lv_hal_indev.h unconditionally defines LV_INDEV_DEF_SCROLL_*. +// - Our shared lv_conf defines LV_INDEV_DEF_SCROLL_LIMIT for touch tuning, and +// M9's build flags define LV_INDEV_DEF_SCROLL_THROW. That combination emits +// macro-redefinition warnings, even though M9 has no touch input path. +// +// For M9, include the shared project LV config, then drop touch-scroll tuning +// macros so LVGL's keypad-only defaults can apply without warnings. +#ifndef LV_CONF_H +#define LV_CONF_H +#endif + +#include "lv_conf.h" + +#ifdef LV_INDEV_DEF_SCROLL_LIMIT +#undef LV_INDEV_DEF_SCROLL_LIMIT +#endif + +#ifdef LV_INDEV_DEF_SCROLL_THROW +#undef LV_INDEV_DEF_SCROLL_THROW +#endif diff --git a/lib/minimp3/LICENSE b/lib/minimp3/LICENSE new file mode 100644 index 0000000..2c4afab --- /dev/null +++ b/lib/minimp3/LICENSE @@ -0,0 +1,117 @@ +CC0 1.0 Universal + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator and +subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the +purpose of contributing to a commons of creative, cultural and scientific +works ("Commons") that the public can reliably and without fear of later +claims of infringement build upon, modify, incorporate in other works, reuse +and redistribute as freely as possible in any form whatsoever and for any +purposes, including without limitation commercial purposes. These owners may +contribute to the Commons to promote the ideal of a free culture and the +further production of creative, cultural and scientific works, or to gain +reputation or greater distribution for their Work in part through the use and +efforts of others. + +For these and/or other purposes and motivations, and without any expectation +of additional consideration or compensation, the person associating CC0 with a +Work (the "Affirmer"), to the extent that he or she is an owner of Copyright +and Related Rights in the Work, voluntarily elects to apply CC0 to the Work +and publicly distribute the Work under its terms, with knowledge of his or her +Copyright and Related Rights in the Work and the meaning and intended legal +effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not limited +to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, communicate, + and translate a Work; + + ii. moral rights retained by the original author(s) and/or performer(s); + + iii. publicity and privacy rights pertaining to a person's image or likeness + depicted in a Work; + + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + + v. rights protecting the extraction, dissemination, use and reuse of data in + a Work; + + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation thereof, + including any amended or successor version of such directive); and + + vii. other similar, equivalent or corresponding rights throughout the world + based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, +applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and +unconditionally waives, abandons, and surrenders all of Affirmer's Copyright +and Related Rights and associated claims and causes of action, whether now +known or unknown (including existing as well as future claims and causes of +action), in the Work (i) in all territories worldwide, (ii) for the maximum +duration provided by applicable law or treaty (including future time +extensions), (iii) in any current or future medium and for any number of +copies, and (iv) for any purpose whatsoever, including without limitation +commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes +the Waiver for the benefit of each member of the public at large and to the +detriment of Affirmer's heirs and successors, fully intending that such Waiver +shall not be subject to revocation, rescission, cancellation, termination, or +any other legal or equitable action to disrupt the quiet enjoyment of the Work +by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be +judged legally invalid or ineffective under applicable law, then the Waiver +shall be preserved to the maximum extent permitted taking into account +Affirmer's express Statement of Purpose. In addition, to the extent the Waiver +is so judged Affirmer hereby grants to each affected person a royalty-free, +non transferable, non sublicensable, non exclusive, irrevocable and +unconditional license to exercise Affirmer's Copyright and Related Rights in +the Work (i) in all territories worldwide, (ii) for the maximum duration +provided by applicable law or treaty (including future time extensions), (iii) +in any current or future medium and for any number of copies, and (iv) for any +purpose whatsoever, including without limitation commercial, advertising or +promotional purposes (the "License"). The License shall be deemed effective as +of the date CC0 was applied by Affirmer to the Work. Should any part of the +License for any reason be judged legally invalid or ineffective under +applicable law, such partial invalidity or ineffectiveness shall not +invalidate the remainder of the License, and in such case Affirmer hereby +affirms that he or she will not (i) exercise any of his or her remaining +Copyright and Related Rights in the Work or (ii) assert any associated claims +and causes of action with respect to the Work, in either case contrary to +Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + + b. Affirmer offers the Work as-is and makes no representations or warranties + of any kind concerning the Work, express, implied, statutory or otherwise, + including without limitation warranties of title, merchantability, fitness + for a particular purpose, non infringement, or the absence of latent or + other defects, accuracy, or the present or absence of errors, whether or not + discoverable, all to the greatest extent permissible under applicable law. + + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without limitation + any person's Copyright and Related Rights in the Work. Further, Affirmer + disclaims responsibility for obtaining any necessary consents, permissions + or other rights required for any use of the Work. + + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to this + CC0 or use of the Work. + +For more information, please see + + diff --git a/lib/minimp3/README.md b/lib/minimp3/README.md new file mode 100644 index 0000000..7fbdc38 --- /dev/null +++ b/lib/minimp3/README.md @@ -0,0 +1,16 @@ +# minimp3 + +This directory vendors `minimp3.h` from +[lieff/minimp3](https://github.com/lieff/minimp3) at commit +`ea99364f61c14656440e8d77e9c233ccf3124633` under CC0-1.0. + +Pinned upstream and local file hashes: + +- upstream `minimp3.h`: `57e437c5c1f0e8b243885d3929c8973b5e6c778451e0100ab4251d19915cb3ad` +- local `minimp3.h` with the hook below: `3f59a7f29de636ca0db0aab8a5b86739c43ac8bf9ac902010e7220f783c0ae0a` +- `LICENSE`: `6a1ee543e5282cd9061881edf462e6fdab181f328da71fc2c9a6950a80e94d01` + +Wadamesh adds one opt-in `MINIMP3_SCRATCH` hook around the decoder's local +scratch object. The Lua audio worker uses it to place the roughly 16 KB decoder +workspace in PSRAM instead of its FreeRTOS task stack. Builds that do not define +the hook retain upstream behavior. diff --git a/lib/minimp3/minimp3.h b/lib/minimp3/minimp3.h new file mode 100644 index 0000000..ffab423 --- /dev/null +++ b/lib/minimp3/minimp3.h @@ -0,0 +1,1872 @@ +#ifndef MINIMP3_H +#define MINIMP3_H +/* + https://github.com/lieff/minimp3 + To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. + This software is distributed without any warranty. + See . +*/ +#include + +#define MINIMP3_MAX_SAMPLES_PER_FRAME (1152*2) + +typedef struct +{ + int frame_bytes, frame_offset, channels, hz, layer, bitrate_kbps; +} mp3dec_frame_info_t; + +typedef struct +{ + float mdct_overlap[2][9*32], qmf_state[15*2*32]; + int reserv, free_format_bytes; + unsigned char header[4], reserv_buf[511]; +} mp3dec_t; + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +void mp3dec_init(mp3dec_t *dec); +#ifndef MINIMP3_FLOAT_OUTPUT +typedef int16_t mp3d_sample_t; +#else /* MINIMP3_FLOAT_OUTPUT */ +typedef float mp3d_sample_t; +void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples); +#endif /* MINIMP3_FLOAT_OUTPUT */ +int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* MINIMP3_H */ +#if defined(MINIMP3_IMPLEMENTATION) && !defined(_MINIMP3_IMPLEMENTATION_GUARD) +#define _MINIMP3_IMPLEMENTATION_GUARD + +#include +#include + +#define MAX_FREE_FORMAT_FRAME_SIZE 2304 /* more than ISO spec's */ +#ifndef MAX_FRAME_SYNC_MATCHES +#define MAX_FRAME_SYNC_MATCHES 10 +#endif /* MAX_FRAME_SYNC_MATCHES */ + +#define MAX_L3_FRAME_PAYLOAD_BYTES MAX_FREE_FORMAT_FRAME_SIZE /* MUST be >= 320000/8/32000*1152 = 1440 */ + +#define MAX_BITRESERVOIR_BYTES 511 +#define SHORT_BLOCK_TYPE 2 +#define STOP_BLOCK_TYPE 3 +#define MODE_MONO 3 +#define MODE_JOINT_STEREO 1 +#define HDR_SIZE 4 +#define HDR_IS_MONO(h) (((h[3]) & 0xC0) == 0xC0) +#define HDR_IS_MS_STEREO(h) (((h[3]) & 0xE0) == 0x60) +#define HDR_IS_FREE_FORMAT(h) (((h[2]) & 0xF0) == 0) +#define HDR_IS_CRC(h) (!((h[1]) & 1)) +#define HDR_TEST_PADDING(h) ((h[2]) & 0x2) +#define HDR_TEST_MPEG1(h) ((h[1]) & 0x8) +#define HDR_TEST_NOT_MPEG25(h) ((h[1]) & 0x10) +#define HDR_TEST_I_STEREO(h) ((h[3]) & 0x10) +#define HDR_TEST_MS_STEREO(h) ((h[3]) & 0x20) +#define HDR_GET_STEREO_MODE(h) (((h[3]) >> 6) & 3) +#define HDR_GET_STEREO_MODE_EXT(h) (((h[3]) >> 4) & 3) +#define HDR_GET_LAYER(h) (((h[1]) >> 1) & 3) +#define HDR_GET_BITRATE(h) ((h[2]) >> 4) +#define HDR_GET_SAMPLE_RATE(h) (((h[2]) >> 2) & 3) +#define HDR_GET_MY_SAMPLE_RATE(h) (HDR_GET_SAMPLE_RATE(h) + (((h[1] >> 3) & 1) + ((h[1] >> 4) & 1))*3) +#define HDR_IS_FRAME_576(h) ((h[1] & 14) == 2) +#define HDR_IS_LAYER_1(h) ((h[1] & 6) == 6) + +#define BITS_DEQUANTIZER_OUT -1 +#define MAX_SCF (255 + BITS_DEQUANTIZER_OUT*4 - 210) +#define MAX_SCFI ((MAX_SCF + 3) & ~3) + +#define MINIMP3_MIN(a, b) ((a) > (b) ? (b) : (a)) +#define MINIMP3_MAX(a, b) ((a) < (b) ? (b) : (a)) + +#if !defined(MINIMP3_NO_SIMD) + +#if !defined(MINIMP3_ONLY_SIMD) && (defined(_M_X64) || defined(__x86_64__) || defined(__aarch64__) || defined(_M_ARM64)) +/* x64 always have SSE2, arm64 always have neon, no need for generic code */ +#define MINIMP3_ONLY_SIMD +#endif /* SIMD checks... */ + +#if (defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))) || ((defined(__i386__) || defined(__x86_64__)) && defined(__SSE2__)) +#if defined(_MSC_VER) +#include +#endif /* defined(_MSC_VER) */ +#include +#define HAVE_SSE 1 +#define HAVE_SIMD 1 +#define VSTORE _mm_storeu_ps +#define VLD _mm_loadu_ps +#define VSET _mm_set1_ps +#define VADD _mm_add_ps +#define VSUB _mm_sub_ps +#define VMUL _mm_mul_ps +#define VMAC(a, x, y) _mm_add_ps(a, _mm_mul_ps(x, y)) +#define VMSB(a, x, y) _mm_sub_ps(a, _mm_mul_ps(x, y)) +#define VMUL_S(x, s) _mm_mul_ps(x, _mm_set1_ps(s)) +#define VREV(x) _mm_shuffle_ps(x, x, _MM_SHUFFLE(0, 1, 2, 3)) +typedef __m128 f4; +#if defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) +#define minimp3_cpuid __cpuid +#else /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */ +static __inline__ __attribute__((always_inline)) void minimp3_cpuid(int CPUInfo[], const int InfoType) +{ +#if defined(__PIC__) + __asm__ __volatile__( +#if defined(__x86_64__) + "push %%rbx\n" + "cpuid\n" + "xchgl %%ebx, %1\n" + "pop %%rbx\n" +#else /* defined(__x86_64__) */ + "xchgl %%ebx, %1\n" + "cpuid\n" + "xchgl %%ebx, %1\n" +#endif /* defined(__x86_64__) */ + : "=a" (CPUInfo[0]), "=r" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#else /* defined(__PIC__) */ + __asm__ __volatile__( + "cpuid" + : "=a" (CPUInfo[0]), "=b" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#endif /* defined(__PIC__)*/ +} +#endif /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */ +static int have_simd(void) +{ +#ifdef MINIMP3_ONLY_SIMD + return 1; +#else /* MINIMP3_ONLY_SIMD */ + static int g_have_simd; + int CPUInfo[4]; +#ifdef MINIMP3_TEST + static int g_counter; + if (g_counter++ > 100) + return 0; +#endif /* MINIMP3_TEST */ + if (g_have_simd) + goto end; + minimp3_cpuid(CPUInfo, 0); + g_have_simd = 1; + if (CPUInfo[0] > 0) + { + minimp3_cpuid(CPUInfo, 1); + g_have_simd = (CPUInfo[3] & (1 << 26)) + 1; /* SSE2 */ + } +end: + return g_have_simd - 1; +#endif /* MINIMP3_ONLY_SIMD */ +} +#elif defined(__ARM_NEON) || defined(__aarch64__) || defined(_M_ARM64) +#include +#define HAVE_SSE 0 +#define HAVE_SIMD 1 +#define VSTORE vst1q_f32 +#define VLD vld1q_f32 +#define VSET vmovq_n_f32 +#define VADD vaddq_f32 +#define VSUB vsubq_f32 +#define VMUL vmulq_f32 +#define VMAC(a, x, y) vmlaq_f32(a, x, y) +#define VMSB(a, x, y) vmlsq_f32(a, x, y) +#define VMUL_S(x, s) vmulq_f32(x, vmovq_n_f32(s)) +#define VREV(x) vcombine_f32(vget_high_f32(vrev64q_f32(x)), vget_low_f32(vrev64q_f32(x))) +typedef float32x4_t f4; +static int have_simd() +{ /* TODO: detect neon for !MINIMP3_ONLY_SIMD */ + return 1; +} +#else /* SIMD checks... */ +#define HAVE_SSE 0 +#define HAVE_SIMD 0 +#ifdef MINIMP3_ONLY_SIMD +#error MINIMP3_ONLY_SIMD used, but SSE/NEON not enabled +#endif /* MINIMP3_ONLY_SIMD */ +#endif /* SIMD checks... */ +#else /* !defined(MINIMP3_NO_SIMD) */ +#define HAVE_SIMD 0 +#endif /* !defined(MINIMP3_NO_SIMD) */ + +#if defined(__ARM_ARCH) && (__ARM_ARCH >= 6) && !defined(__aarch64__) && !defined(_M_ARM64) && !defined(__ARM_ARCH_6M__) +#define HAVE_ARMV6 1 +static __inline__ __attribute__((always_inline)) int32_t minimp3_clip_int16_arm(int32_t a) +{ + int32_t x = 0; + __asm__ ("ssat %0, #16, %1" : "=r"(x) : "r"(a)); + return x; +} +#else +#define HAVE_ARMV6 0 +#endif + +typedef struct +{ + const uint8_t *buf; + int pos, limit; +} bs_t; + +typedef struct +{ + float scf[3*64]; + uint8_t total_bands, stereo_bands, bitalloc[64], scfcod[64]; +} L12_scale_info; + +typedef struct +{ + uint8_t tab_offset, code_tab_width, band_count; +} L12_subband_alloc_t; + +typedef struct +{ + const uint8_t *sfbtab; + uint16_t part_23_length, big_values, scalefac_compress; + uint8_t global_gain, block_type, mixed_block_flag, n_long_sfb, n_short_sfb; + uint8_t table_select[3], region_count[3], subblock_gain[3]; + uint8_t preflag, scalefac_scale, count1_table, scfsi; +} L3_gr_info_t; + +typedef struct +{ + bs_t bs; + uint8_t maindata[MAX_BITRESERVOIR_BYTES + MAX_L3_FRAME_PAYLOAD_BYTES]; + L3_gr_info_t gr_info[4]; + float grbuf[2][576], scf[40], syn[18 + 15][2*32]; + uint8_t ist_pos[2][39]; +} mp3dec_scratch_t; + +static void bs_init(bs_t *bs, const uint8_t *data, int bytes) +{ + bs->buf = data; + bs->pos = 0; + bs->limit = bytes*8; +} + +static uint32_t get_bits(bs_t *bs, int n) +{ + uint32_t next, cache = 0, s = bs->pos & 7; + int shl = n + s; + const uint8_t *p = bs->buf + (bs->pos >> 3); + if ((bs->pos += n) > bs->limit) + return 0; + next = *p++ & (255 >> s); + while ((shl -= 8) > 0) + { + cache |= next << shl; + next = *p++; + } + return cache | (next >> -shl); +} + +static int hdr_valid(const uint8_t *h) +{ + return h[0] == 0xff && + ((h[1] & 0xF0) == 0xf0 || (h[1] & 0xFE) == 0xe2) && + (HDR_GET_LAYER(h) != 0) && + (HDR_GET_BITRATE(h) != 15) && + (HDR_GET_SAMPLE_RATE(h) != 3); +} + +static int hdr_compare(const uint8_t *h1, const uint8_t *h2) +{ + return hdr_valid(h2) && + ((h1[1] ^ h2[1]) & 0xFE) == 0 && + ((h1[2] ^ h2[2]) & 0x0C) == 0 && + !(HDR_IS_FREE_FORMAT(h1) ^ HDR_IS_FREE_FORMAT(h2)); +} + +static unsigned hdr_bitrate_kbps(const uint8_t *h) +{ + static const uint8_t halfrate[2][3][15] = { + { { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,16,24,28,32,40,48,56,64,72,80,88,96,112,128 } }, + { { 0,16,20,24,28,32,40,48,56,64,80,96,112,128,160 }, { 0,16,24,28,32,40,48,56,64,80,96,112,128,160,192 }, { 0,16,32,48,64,80,96,112,128,144,160,176,192,208,224 } }, + }; + return 2*halfrate[!!HDR_TEST_MPEG1(h)][HDR_GET_LAYER(h) - 1][HDR_GET_BITRATE(h)]; +} + +static unsigned hdr_sample_rate_hz(const uint8_t *h) +{ + static const unsigned g_hz[3] = { 44100, 48000, 32000 }; + return g_hz[HDR_GET_SAMPLE_RATE(h)] >> (int)!HDR_TEST_MPEG1(h) >> (int)!HDR_TEST_NOT_MPEG25(h); +} + +static unsigned hdr_frame_samples(const uint8_t *h) +{ + return HDR_IS_LAYER_1(h) ? 384 : (1152 >> (int)HDR_IS_FRAME_576(h)); +} + +static int hdr_frame_bytes(const uint8_t *h, int free_format_size) +{ + int frame_bytes = hdr_frame_samples(h)*hdr_bitrate_kbps(h)*125/hdr_sample_rate_hz(h); + if (HDR_IS_LAYER_1(h)) + { + frame_bytes &= ~3; /* slot align */ + } + return frame_bytes ? frame_bytes : free_format_size; +} + +static int hdr_padding(const uint8_t *h) +{ + return HDR_TEST_PADDING(h) ? (HDR_IS_LAYER_1(h) ? 4 : 1) : 0; +} + +#ifndef MINIMP3_ONLY_MP3 +static const L12_subband_alloc_t *L12_subband_alloc_table(const uint8_t *hdr, L12_scale_info *sci) +{ + const L12_subband_alloc_t *alloc; + int mode = HDR_GET_STEREO_MODE(hdr); + int nbands, stereo_bands = (mode == MODE_MONO) ? 0 : (mode == MODE_JOINT_STEREO) ? (HDR_GET_STEREO_MODE_EXT(hdr) << 2) + 4 : 32; + + if (HDR_IS_LAYER_1(hdr)) + { + static const L12_subband_alloc_t g_alloc_L1[] = { { 76, 4, 32 } }; + alloc = g_alloc_L1; + nbands = 32; + } else if (!HDR_TEST_MPEG1(hdr)) + { + static const L12_subband_alloc_t g_alloc_L2M2[] = { { 60, 4, 4 }, { 44, 3, 7 }, { 44, 2, 19 } }; + alloc = g_alloc_L2M2; + nbands = 30; + } else + { + static const L12_subband_alloc_t g_alloc_L2M1[] = { { 0, 4, 3 }, { 16, 4, 8 }, { 32, 3, 12 }, { 40, 2, 7 } }; + int sample_rate_idx = HDR_GET_SAMPLE_RATE(hdr); + unsigned kbps = hdr_bitrate_kbps(hdr) >> (int)(mode != MODE_MONO); + if (!kbps) /* free-format */ + { + kbps = 192; + } + + alloc = g_alloc_L2M1; + nbands = 27; + if (kbps < 56) + { + static const L12_subband_alloc_t g_alloc_L2M1_lowrate[] = { { 44, 4, 2 }, { 44, 3, 10 } }; + alloc = g_alloc_L2M1_lowrate; + nbands = sample_rate_idx == 2 ? 12 : 8; + } else if (kbps >= 96 && sample_rate_idx != 1) + { + nbands = 30; + } + } + + sci->total_bands = (uint8_t)nbands; + sci->stereo_bands = (uint8_t)MINIMP3_MIN(stereo_bands, nbands); + + return alloc; +} + +static void L12_read_scalefactors(bs_t *bs, uint8_t *pba, uint8_t *scfcod, int bands, float *scf) +{ + static const float g_deq_L12[18*3] = { +#define DQ(x) 9.53674316e-07f/x, 7.56931807e-07f/x, 6.00777173e-07f/x + DQ(3),DQ(7),DQ(15),DQ(31),DQ(63),DQ(127),DQ(255),DQ(511),DQ(1023),DQ(2047),DQ(4095),DQ(8191),DQ(16383),DQ(32767),DQ(65535),DQ(3),DQ(5),DQ(9) + }; + int i, m; + for (i = 0; i < bands; i++) + { + float s = 0; + int ba = *pba++; + int mask = ba ? 4 + ((19 >> scfcod[i]) & 3) : 0; + for (m = 4; m; m >>= 1) + { + if (mask & m) + { + int b = get_bits(bs, 6); + s = g_deq_L12[ba*3 - 6 + b % 3]*(1 << 21 >> b/3); + } + *scf++ = s; + } + } +} + +static void L12_read_scale_info(const uint8_t *hdr, bs_t *bs, L12_scale_info *sci) +{ + static const uint8_t g_bitalloc_code_tab[] = { + 0,17, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,16, + 0,17,18, 3,19,4,5,16, + 0,17,18,16, + 0,17,18,19, 4,5,6, 7,8, 9,10,11,12,13,14,15, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,14, + 0, 2, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16 + }; + const L12_subband_alloc_t *subband_alloc = L12_subband_alloc_table(hdr, sci); + + int i, k = 0, ba_bits = 0; + const uint8_t *ba_code_tab = g_bitalloc_code_tab; + + for (i = 0; i < sci->total_bands; i++) + { + uint8_t ba; + if (i == k) + { + k += subband_alloc->band_count; + ba_bits = subband_alloc->code_tab_width; + ba_code_tab = g_bitalloc_code_tab + subband_alloc->tab_offset; + subband_alloc++; + } + ba = ba_code_tab[get_bits(bs, ba_bits)]; + sci->bitalloc[2*i] = ba; + if (i < sci->stereo_bands) + { + ba = ba_code_tab[get_bits(bs, ba_bits)]; + } + sci->bitalloc[2*i + 1] = sci->stereo_bands ? ba : 0; + } + + for (i = 0; i < 2*sci->total_bands; i++) + { + sci->scfcod[i] = sci->bitalloc[i] ? HDR_IS_LAYER_1(hdr) ? 2 : get_bits(bs, 2) : 6; + } + + L12_read_scalefactors(bs, sci->bitalloc, sci->scfcod, sci->total_bands*2, sci->scf); + + for (i = sci->stereo_bands; i < sci->total_bands; i++) + { + sci->bitalloc[2*i + 1] = 0; + } +} + +static int L12_dequantize_granule(float *grbuf, bs_t *bs, L12_scale_info *sci, int group_size) +{ + int i, j, k, choff = 576; + for (j = 0; j < 4; j++) + { + float *dst = grbuf + group_size*j; + for (i = 0; i < 2*sci->total_bands; i++) + { + int ba = sci->bitalloc[i]; + if (ba != 0) + { + if (ba < 17) + { + int half = (1 << (ba - 1)) - 1; + for (k = 0; k < group_size; k++) + { + dst[k] = (float)((int)get_bits(bs, ba) - half); + } + } else + { + unsigned mod = (2 << (ba - 17)) + 1; /* 3, 5, 9 */ + unsigned code = get_bits(bs, mod + 2 - (mod >> 3)); /* 5, 7, 10 */ + for (k = 0; k < group_size; k++, code /= mod) + { + dst[k] = (float)((int)(code % mod - mod/2)); + } + } + } + dst += choff; + choff = 18 - choff; + } + } + return group_size*4; +} + +static void L12_apply_scf_384(L12_scale_info *sci, const float *scf, float *dst) +{ + int i, k; + memcpy(dst + 576 + sci->stereo_bands*18, dst + sci->stereo_bands*18, (sci->total_bands - sci->stereo_bands)*18*sizeof(float)); + for (i = 0; i < sci->total_bands; i++, dst += 18, scf += 6) + { + for (k = 0; k < 12; k++) + { + dst[k + 0] *= scf[0]; + dst[k + 576] *= scf[3]; + } + } +} +#endif /* MINIMP3_ONLY_MP3 */ + +static int L3_read_side_info(bs_t *bs, L3_gr_info_t *gr, const uint8_t *hdr) +{ + static const uint8_t g_scf_long[8][23] = { + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 12,12,12,12,12,12,16,20,24,28,32,40,48,56,64,76,90,2,2,2,2,2,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,18,22,26,32,38,46,54,62,70,76,36,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 4,4,4,4,4,4,6,6,8,8,10,12,16,20,24,28,34,42,50,54,76,158,0 }, + { 4,4,4,4,4,4,6,6,6,8,10,12,16,18,22,28,34,40,46,54,54,192,0 }, + { 4,4,4,4,4,4,6,6,8,10,12,16,20,24,30,38,46,56,68,84,102,26,0 } + }; + static const uint8_t g_scf_short[8][40] = { + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 8,8,8,8,8,8,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + static const uint8_t g_scf_mixed[8][40] = { + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 12,12,12,4,4,4,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 6,6,6,6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + + unsigned tables, scfsi = 0; + int main_data_begin, part_23_sum = 0; + int sr_idx = HDR_GET_MY_SAMPLE_RATE(hdr); sr_idx -= (sr_idx != 0); + int gr_count = HDR_IS_MONO(hdr) ? 1 : 2; + + if (HDR_TEST_MPEG1(hdr)) + { + gr_count *= 2; + main_data_begin = get_bits(bs, 9); + scfsi = get_bits(bs, 7 + gr_count); + } else + { + main_data_begin = get_bits(bs, 8 + gr_count) >> gr_count; + } + + do + { + if (HDR_IS_MONO(hdr)) + { + scfsi <<= 4; + } + gr->part_23_length = (uint16_t)get_bits(bs, 12); + part_23_sum += gr->part_23_length; + gr->big_values = (uint16_t)get_bits(bs, 9); + if (gr->big_values > 288) + { + return -1; + } + gr->global_gain = (uint8_t)get_bits(bs, 8); + gr->scalefac_compress = (uint16_t)get_bits(bs, HDR_TEST_MPEG1(hdr) ? 4 : 9); + gr->sfbtab = g_scf_long[sr_idx]; + gr->n_long_sfb = 22; + gr->n_short_sfb = 0; + if (get_bits(bs, 1)) + { + gr->block_type = (uint8_t)get_bits(bs, 2); + if (!gr->block_type) + { + return -1; + } + gr->mixed_block_flag = (uint8_t)get_bits(bs, 1); + gr->region_count[0] = 7; + gr->region_count[1] = 255; + if (gr->block_type == SHORT_BLOCK_TYPE) + { + scfsi &= 0x0F0F; + if (!gr->mixed_block_flag) + { + gr->region_count[0] = 8; + gr->sfbtab = g_scf_short[sr_idx]; + gr->n_long_sfb = 0; + gr->n_short_sfb = 39; + } else + { + gr->sfbtab = g_scf_mixed[sr_idx]; + gr->n_long_sfb = HDR_TEST_MPEG1(hdr) ? 8 : 6; + gr->n_short_sfb = 30; + } + } + tables = get_bits(bs, 10); + tables <<= 5; + gr->subblock_gain[0] = (uint8_t)get_bits(bs, 3); + gr->subblock_gain[1] = (uint8_t)get_bits(bs, 3); + gr->subblock_gain[2] = (uint8_t)get_bits(bs, 3); + } else + { + gr->block_type = 0; + gr->mixed_block_flag = 0; + tables = get_bits(bs, 15); + gr->region_count[0] = (uint8_t)get_bits(bs, 4); + gr->region_count[1] = (uint8_t)get_bits(bs, 3); + gr->region_count[2] = 255; + } + gr->table_select[0] = (uint8_t)(tables >> 10); + gr->table_select[1] = (uint8_t)((tables >> 5) & 31); + gr->table_select[2] = (uint8_t)((tables) & 31); + gr->preflag = HDR_TEST_MPEG1(hdr) ? get_bits(bs, 1) : (gr->scalefac_compress >= 500); + gr->scalefac_scale = (uint8_t)get_bits(bs, 1); + gr->count1_table = (uint8_t)get_bits(bs, 1); + gr->scfsi = (uint8_t)((scfsi >> 12) & 15); + scfsi <<= 4; + gr++; + } while(--gr_count); + + if (part_23_sum + bs->pos > bs->limit + main_data_begin*8) + { + return -1; + } + + return main_data_begin; +} + +static void L3_read_scalefactors(uint8_t *scf, uint8_t *ist_pos, const uint8_t *scf_size, const uint8_t *scf_count, bs_t *bitbuf, int scfsi) +{ + int i, k; + for (i = 0; i < 4 && scf_count[i]; i++, scfsi *= 2) + { + int cnt = scf_count[i]; + if (scfsi & 8) + { + memcpy(scf, ist_pos, cnt); + } else + { + int bits = scf_size[i]; + if (!bits) + { + memset(scf, 0, cnt); + memset(ist_pos, 0, cnt); + } else + { + int max_scf = (scfsi < 0) ? (1 << bits) - 1 : -1; + for (k = 0; k < cnt; k++) + { + int s = get_bits(bitbuf, bits); + ist_pos[k] = (s == max_scf ? -1 : s); + scf[k] = s; + } + } + } + ist_pos += cnt; + scf += cnt; + } + scf[0] = scf[1] = scf[2] = 0; +} + +static float L3_ldexp_q2(float y, int exp_q2) +{ + static const float g_expfrac[4] = { 9.31322575e-10f,7.83145814e-10f,6.58544508e-10f,5.53767716e-10f }; + int e; + do + { + e = MINIMP3_MIN(30*4, exp_q2); + y *= g_expfrac[e & 3]*(1 << 30 >> (e >> 2)); + } while ((exp_q2 -= e) > 0); + return y; +} + +static void L3_decode_scalefactors(const uint8_t *hdr, uint8_t *ist_pos, bs_t *bs, const L3_gr_info_t *gr, float *scf, int ch) +{ + static const uint8_t g_scf_partitions[3][28] = { + { 6,5,5, 5,6,5,5,5,6,5, 7,3,11,10,0,0, 7, 7, 7,0, 6, 6,6,3, 8, 8,5,0 }, + { 8,9,6,12,6,9,9,9,6,9,12,6,15,18,0,0, 6,15,12,0, 6,12,9,6, 6,18,9,0 }, + { 9,9,6,12,9,9,9,9,9,9,12,6,18,18,0,0,12,12,12,0,12, 9,9,6,15,12,9,0 } + }; + const uint8_t *scf_partition = g_scf_partitions[!!gr->n_short_sfb + !gr->n_long_sfb]; + uint8_t scf_size[4], iscf[40]; + int i, scf_shift = gr->scalefac_scale + 1, gain_exp, scfsi = gr->scfsi; + float gain; + + if (HDR_TEST_MPEG1(hdr)) + { + static const uint8_t g_scfc_decode[16] = { 0,1,2,3, 12,5,6,7, 9,10,11,13, 14,15,18,19 }; + int part = g_scfc_decode[gr->scalefac_compress]; + scf_size[1] = scf_size[0] = (uint8_t)(part >> 2); + scf_size[3] = scf_size[2] = (uint8_t)(part & 3); + } else + { + static const uint8_t g_mod[6*4] = { 5,5,4,4,5,5,4,1,4,3,1,1,5,6,6,1,4,4,4,1,4,3,1,1 }; + int k, modprod, sfc, ist = HDR_TEST_I_STEREO(hdr) && ch; + sfc = gr->scalefac_compress >> ist; + for (k = ist*3*4; sfc >= 0; sfc -= modprod, k += 4) + { + for (modprod = 1, i = 3; i >= 0; i--) + { + scf_size[i] = (uint8_t)(sfc / modprod % g_mod[k + i]); + modprod *= g_mod[k + i]; + } + } + scf_partition += k; + scfsi = -16; + } + L3_read_scalefactors(iscf, ist_pos, scf_size, scf_partition, bs, scfsi); + + if (gr->n_short_sfb) + { + int sh = 3 - scf_shift; + for (i = 0; i < gr->n_short_sfb; i += 3) + { + iscf[gr->n_long_sfb + i + 0] += gr->subblock_gain[0] << sh; + iscf[gr->n_long_sfb + i + 1] += gr->subblock_gain[1] << sh; + iscf[gr->n_long_sfb + i + 2] += gr->subblock_gain[2] << sh; + } + } else if (gr->preflag) + { + static const uint8_t g_preamp[10] = { 1,1,1,1,2,2,3,3,3,2 }; + for (i = 0; i < 10; i++) + { + iscf[11 + i] += g_preamp[i]; + } + } + + gain_exp = gr->global_gain + BITS_DEQUANTIZER_OUT*4 - 210 - (HDR_IS_MS_STEREO(hdr) ? 2 : 0); + gain = L3_ldexp_q2(1 << (MAX_SCFI/4), MAX_SCFI - gain_exp); + for (i = 0; i < (int)(gr->n_long_sfb + gr->n_short_sfb); i++) + { + scf[i] = L3_ldexp_q2(gain, iscf[i] << scf_shift); + } +} + +static const float g_pow43[129 + 16] = { + 0,-1,-2.519842f,-4.326749f,-6.349604f,-8.549880f,-10.902724f,-13.390518f,-16.000000f,-18.720754f,-21.544347f,-24.463781f,-27.473142f,-30.567351f,-33.741992f,-36.993181f, + 0,1,2.519842f,4.326749f,6.349604f,8.549880f,10.902724f,13.390518f,16.000000f,18.720754f,21.544347f,24.463781f,27.473142f,30.567351f,33.741992f,36.993181f,40.317474f,43.711787f,47.173345f,50.699631f,54.288352f,57.937408f,61.644865f,65.408941f,69.227979f,73.100443f,77.024898f,81.000000f,85.024491f,89.097188f,93.216975f,97.382800f,101.593667f,105.848633f,110.146801f,114.487321f,118.869381f,123.292209f,127.755065f,132.257246f,136.798076f,141.376907f,145.993119f,150.646117f,155.335327f,160.060199f,164.820202f,169.614826f,174.443577f,179.305980f,184.201575f,189.129918f,194.090580f,199.083145f,204.107210f,209.162385f,214.248292f,219.364564f,224.510845f,229.686789f,234.892058f,240.126328f,245.389280f,250.680604f,256.000000f,261.347174f,266.721841f,272.123723f,277.552547f,283.008049f,288.489971f,293.998060f,299.532071f,305.091761f,310.676898f,316.287249f,321.922592f,327.582707f,333.267377f,338.976394f,344.709550f,350.466646f,356.247482f,362.051866f,367.879608f,373.730522f,379.604427f,385.501143f,391.420496f,397.362314f,403.326427f,409.312672f,415.320884f,421.350905f,427.402579f,433.475750f,439.570269f,445.685987f,451.822757f,457.980436f,464.158883f,470.357960f,476.577530f,482.817459f,489.077615f,495.357868f,501.658090f,507.978156f,514.317941f,520.677324f,527.056184f,533.454404f,539.871867f,546.308458f,552.764065f,559.238575f,565.731879f,572.243870f,578.774440f,585.323483f,591.890898f,598.476581f,605.080431f,611.702349f,618.342238f,625.000000f,631.675540f,638.368763f,645.079578f +}; + +static float L3_pow_43(int x) +{ + float frac; + int sign, mult = 256; + + if (x < 129) + { + return g_pow43[16 + x]; + } + + if (x < 1024) + { + mult = 16; + x <<= 3; + } + + sign = 2*x & 64; + frac = (float)((x & 63) - sign) / ((x & ~63) + sign); + return g_pow43[16 + ((x + sign) >> 6)]*(1.f + frac*((4.f/3) + frac*(2.f/9)))*mult; +} + +static void L3_huffman(float *dst, bs_t *bs, const L3_gr_info_t *gr_info, const float *scf, int layer3gr_limit) +{ + static const int16_t tabs[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 785,785,785,785,784,784,784,784,513,513,513,513,513,513,513,513,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256, + -255,1313,1298,1282,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,290,288, + -255,1313,1298,1282,769,769,769,769,529,529,529,529,529,529,529,529,528,528,528,528,528,528,528,528,512,512,512,512,512,512,512,512,290,288, + -253,-318,-351,-367,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,819,818,547,547,275,275,275,275,561,560,515,546,289,274,288,258, + -254,-287,1329,1299,1314,1312,1057,1057,1042,1042,1026,1026,784,784,784,784,529,529,529,529,529,529,529,529,769,769,769,769,768,768,768,768,563,560,306,306,291,259, + -252,-413,-477,-542,1298,-575,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-383,-399,1107,1092,1106,1061,849,849,789,789,1104,1091,773,773,1076,1075,341,340,325,309,834,804,577,577,532,532,516,516,832,818,803,816,561,561,531,531,515,546,289,289,288,258, + -252,-429,-493,-559,1057,1057,1042,1042,529,529,529,529,529,529,529,529,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,-382,1077,-415,1106,1061,1104,849,849,789,789,1091,1076,1029,1075,834,834,597,581,340,340,339,324,804,833,532,532,832,772,818,803,817,787,816,771,290,290,290,290,288,258, + -253,-349,-414,-447,-463,1329,1299,-479,1314,1312,1057,1057,1042,1042,1026,1026,785,785,785,785,784,784,784,784,769,769,769,769,768,768,768,768,-319,851,821,-335,836,850,805,849,341,340,325,336,533,533,579,579,564,564,773,832,578,548,563,516,321,276,306,291,304,259, + -251,-572,-733,-830,-863,-879,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,1396,1351,1381,1366,1395,1335,1380,-559,1334,1138,1138,1063,1063,1350,1392,1031,1031,1062,1062,1364,1363,1120,1120,1333,1348,881,881,881,881,375,374,359,373,343,358,341,325,791,791,1123,1122,-703,1105,1045,-719,865,865,790,790,774,774,1104,1029,338,293,323,308,-799,-815,833,788,772,818,803,816,322,292,307,320,561,531,515,546,289,274,288,258, + -251,-525,-605,-685,-765,-831,-846,1298,1057,1057,1312,1282,785,785,785,785,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,1399,1398,1383,1367,1382,1396,1351,-511,1381,1366,1139,1139,1079,1079,1124,1124,1364,1349,1363,1333,882,882,882,882,807,807,807,807,1094,1094,1136,1136,373,341,535,535,881,775,867,822,774,-591,324,338,-671,849,550,550,866,864,609,609,293,336,534,534,789,835,773,-751,834,804,308,307,833,788,832,772,562,562,547,547,305,275,560,515,290,290, + -252,-397,-477,-557,-622,-653,-719,-735,-750,1329,1299,1314,1057,1057,1042,1042,1312,1282,1024,1024,785,785,785,785,784,784,784,784,769,769,769,769,-383,1127,1141,1111,1126,1140,1095,1110,869,869,883,883,1079,1109,882,882,375,374,807,868,838,881,791,-463,867,822,368,263,852,837,836,-543,610,610,550,550,352,336,534,534,865,774,851,821,850,805,593,533,579,564,773,832,578,578,548,548,577,577,307,276,306,291,516,560,259,259, + -250,-2107,-2507,-2764,-2909,-2974,-3007,-3023,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-767,-1052,-1213,-1277,-1358,-1405,-1469,-1535,-1550,-1582,-1614,-1647,-1662,-1694,-1726,-1759,-1774,-1807,-1822,-1854,-1886,1565,-1919,-1935,-1951,-1967,1731,1730,1580,1717,-1983,1729,1564,-1999,1548,-2015,-2031,1715,1595,-2047,1714,-2063,1610,-2079,1609,-2095,1323,1323,1457,1457,1307,1307,1712,1547,1641,1700,1699,1594,1685,1625,1442,1442,1322,1322,-780,-973,-910,1279,1278,1277,1262,1276,1261,1275,1215,1260,1229,-959,974,974,989,989,-943,735,478,478,495,463,506,414,-1039,1003,958,1017,927,942,987,957,431,476,1272,1167,1228,-1183,1256,-1199,895,895,941,941,1242,1227,1212,1135,1014,1014,490,489,503,487,910,1013,985,925,863,894,970,955,1012,847,-1343,831,755,755,984,909,428,366,754,559,-1391,752,486,457,924,997,698,698,983,893,740,740,908,877,739,739,667,667,953,938,497,287,271,271,683,606,590,712,726,574,302,302,738,736,481,286,526,725,605,711,636,724,696,651,589,681,666,710,364,467,573,695,466,466,301,465,379,379,709,604,665,679,316,316,634,633,436,436,464,269,424,394,452,332,438,363,347,408,393,448,331,422,362,407,392,421,346,406,391,376,375,359,1441,1306,-2367,1290,-2383,1337,-2399,-2415,1426,1321,-2431,1411,1336,-2447,-2463,-2479,1169,1169,1049,1049,1424,1289,1412,1352,1319,-2495,1154,1154,1064,1064,1153,1153,416,390,360,404,403,389,344,374,373,343,358,372,327,357,342,311,356,326,1395,1394,1137,1137,1047,1047,1365,1392,1287,1379,1334,1364,1349,1378,1318,1363,792,792,792,792,1152,1152,1032,1032,1121,1121,1046,1046,1120,1120,1030,1030,-2895,1106,1061,1104,849,849,789,789,1091,1076,1029,1090,1060,1075,833,833,309,324,532,532,832,772,818,803,561,561,531,560,515,546,289,274,288,258, + -250,-1179,-1579,-1836,-1996,-2124,-2253,-2333,-2413,-2477,-2542,-2574,-2607,-2622,-2655,1314,1313,1298,1312,1282,785,785,785,785,1040,1040,1025,1025,768,768,768,768,-766,-798,-830,-862,-895,-911,-927,-943,-959,-975,-991,-1007,-1023,-1039,-1055,-1070,1724,1647,-1103,-1119,1631,1767,1662,1738,1708,1723,-1135,1780,1615,1779,1599,1677,1646,1778,1583,-1151,1777,1567,1737,1692,1765,1722,1707,1630,1751,1661,1764,1614,1736,1676,1763,1750,1645,1598,1721,1691,1762,1706,1582,1761,1566,-1167,1749,1629,767,766,751,765,494,494,735,764,719,749,734,763,447,447,748,718,477,506,431,491,446,476,461,505,415,430,475,445,504,399,460,489,414,503,383,474,429,459,502,502,746,752,488,398,501,473,413,472,486,271,480,270,-1439,-1455,1357,-1471,-1487,-1503,1341,1325,-1519,1489,1463,1403,1309,-1535,1372,1448,1418,1476,1356,1462,1387,-1551,1475,1340,1447,1402,1386,-1567,1068,1068,1474,1461,455,380,468,440,395,425,410,454,364,467,466,464,453,269,409,448,268,432,1371,1473,1432,1417,1308,1460,1355,1446,1459,1431,1083,1083,1401,1416,1458,1445,1067,1067,1370,1457,1051,1051,1291,1430,1385,1444,1354,1415,1400,1443,1082,1082,1173,1113,1186,1066,1185,1050,-1967,1158,1128,1172,1097,1171,1081,-1983,1157,1112,416,266,375,400,1170,1142,1127,1065,793,793,1169,1033,1156,1096,1141,1111,1155,1080,1126,1140,898,898,808,808,897,897,792,792,1095,1152,1032,1125,1110,1139,1079,1124,882,807,838,881,853,791,-2319,867,368,263,822,852,837,866,806,865,-2399,851,352,262,534,534,821,836,594,594,549,549,593,593,533,533,848,773,579,579,564,578,548,563,276,276,577,576,306,291,516,560,305,305,275,259, + -251,-892,-2058,-2620,-2828,-2957,-3023,-3039,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,-559,1530,-575,-591,1528,1527,1407,1526,1391,1023,1023,1023,1023,1525,1375,1268,1268,1103,1103,1087,1087,1039,1039,1523,-604,815,815,815,815,510,495,509,479,508,463,507,447,431,505,415,399,-734,-782,1262,-815,1259,1244,-831,1258,1228,-847,-863,1196,-879,1253,987,987,748,-767,493,493,462,477,414,414,686,669,478,446,461,445,474,429,487,458,412,471,1266,1264,1009,1009,799,799,-1019,-1276,-1452,-1581,-1677,-1757,-1821,-1886,-1933,-1997,1257,1257,1483,1468,1512,1422,1497,1406,1467,1496,1421,1510,1134,1134,1225,1225,1466,1451,1374,1405,1252,1252,1358,1480,1164,1164,1251,1251,1238,1238,1389,1465,-1407,1054,1101,-1423,1207,-1439,830,830,1248,1038,1237,1117,1223,1148,1236,1208,411,426,395,410,379,269,1193,1222,1132,1235,1221,1116,976,976,1192,1162,1177,1220,1131,1191,963,963,-1647,961,780,-1663,558,558,994,993,437,408,393,407,829,978,813,797,947,-1743,721,721,377,392,844,950,828,890,706,706,812,859,796,960,948,843,934,874,571,571,-1919,690,555,689,421,346,539,539,944,779,918,873,932,842,903,888,570,570,931,917,674,674,-2575,1562,-2591,1609,-2607,1654,1322,1322,1441,1441,1696,1546,1683,1593,1669,1624,1426,1426,1321,1321,1639,1680,1425,1425,1305,1305,1545,1668,1608,1623,1667,1592,1638,1666,1320,1320,1652,1607,1409,1409,1304,1304,1288,1288,1664,1637,1395,1395,1335,1335,1622,1636,1394,1394,1319,1319,1606,1621,1392,1392,1137,1137,1137,1137,345,390,360,375,404,373,1047,-2751,-2767,-2783,1062,1121,1046,-2799,1077,-2815,1106,1061,789,789,1105,1104,263,355,310,340,325,354,352,262,339,324,1091,1076,1029,1090,1060,1075,833,833,788,788,1088,1028,818,818,803,803,561,561,531,531,816,771,546,546,289,274,288,258, + -253,-317,-381,-446,-478,-509,1279,1279,-811,-1179,-1451,-1756,-1900,-2028,-2189,-2253,-2333,-2414,-2445,-2511,-2526,1313,1298,-2559,1041,1041,1040,1040,1025,1025,1024,1024,1022,1007,1021,991,1020,975,1019,959,687,687,1018,1017,671,671,655,655,1016,1015,639,639,758,758,623,623,757,607,756,591,755,575,754,559,543,543,1009,783,-575,-621,-685,-749,496,-590,750,749,734,748,974,989,1003,958,988,973,1002,942,987,957,972,1001,926,986,941,971,956,1000,910,985,925,999,894,970,-1071,-1087,-1102,1390,-1135,1436,1509,1451,1374,-1151,1405,1358,1480,1420,-1167,1507,1494,1389,1342,1465,1435,1450,1326,1505,1310,1493,1373,1479,1404,1492,1464,1419,428,443,472,397,736,526,464,464,486,457,442,471,484,482,1357,1449,1434,1478,1388,1491,1341,1490,1325,1489,1463,1403,1309,1477,1372,1448,1418,1433,1476,1356,1462,1387,-1439,1475,1340,1447,1402,1474,1324,1461,1371,1473,269,448,1432,1417,1308,1460,-1711,1459,-1727,1441,1099,1099,1446,1386,1431,1401,-1743,1289,1083,1083,1160,1160,1458,1445,1067,1067,1370,1457,1307,1430,1129,1129,1098,1098,268,432,267,416,266,400,-1887,1144,1187,1082,1173,1113,1186,1066,1050,1158,1128,1143,1172,1097,1171,1081,420,391,1157,1112,1170,1142,1127,1065,1169,1049,1156,1096,1141,1111,1155,1080,1126,1154,1064,1153,1140,1095,1048,-2159,1125,1110,1137,-2175,823,823,1139,1138,807,807,384,264,368,263,868,838,853,791,867,822,852,837,866,806,865,790,-2319,851,821,836,352,262,850,805,849,-2399,533,533,835,820,336,261,578,548,563,577,532,532,832,772,562,562,547,547,305,275,560,515,290,290,288,258 }; + static const uint8_t tab32[] = { 130,162,193,209,44,28,76,140,9,9,9,9,9,9,9,9,190,254,222,238,126,94,157,157,109,61,173,205 }; + static const uint8_t tab33[] = { 252,236,220,204,188,172,156,140,124,108,92,76,60,44,28,12 }; + static const int16_t tabindex[2*16] = { 0,32,64,98,0,132,180,218,292,364,426,538,648,746,0,1126,1460,1460,1460,1460,1460,1460,1460,1460,1842,1842,1842,1842,1842,1842,1842,1842 }; + static const uint8_t g_linbits[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,6,8,10,13,4,5,6,7,8,9,11,13 }; + +#define PEEK_BITS(n) (bs_cache >> (32 - n)) +#define FLUSH_BITS(n) { bs_cache <<= (n); bs_sh += (n); } +#define CHECK_BITS while (bs_sh >= 0) { bs_cache |= (uint32_t)*bs_next_ptr++ << bs_sh; bs_sh -= 8; } +#define BSPOS ((bs_next_ptr - bs->buf)*8 - 24 + bs_sh) + + float one = 0.0f; + int ireg = 0, big_val_cnt = gr_info->big_values; + const uint8_t *sfb = gr_info->sfbtab; + const uint8_t *bs_next_ptr = bs->buf + bs->pos/8; + uint32_t bs_cache = (((bs_next_ptr[0]*256u + bs_next_ptr[1])*256u + bs_next_ptr[2])*256u + bs_next_ptr[3]) << (bs->pos & 7); + int pairs_to_decode, np, bs_sh = (bs->pos & 7) - 8; + bs_next_ptr += 4; + + while (big_val_cnt > 0) + { + int tab_num = gr_info->table_select[ireg]; + int sfb_cnt = gr_info->region_count[ireg++]; + const int16_t *codebook = tabs + tabindex[tab_num]; + int linbits = g_linbits[tab_num]; + if (linbits) + { + do + { + np = *sfb++ / 2; + pairs_to_decode = MINIMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[PEEK_BITS(w)]; + while (leaf < 0) + { + FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[PEEK_BITS(w) - (leaf >> 3)]; + } + FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + if (lsb == 15) + { + lsb += PEEK_BITS(linbits); + FLUSH_BITS(linbits); + CHECK_BITS; + *dst = one*L3_pow_43(lsb)*((int32_t)bs_cache < 0 ? -1: 1); + } else + { + *dst = g_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + } + FLUSH_BITS(lsb ? 1 : 0); + } + CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } else + { + do + { + np = *sfb++ / 2; + pairs_to_decode = MINIMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[PEEK_BITS(w)]; + while (leaf < 0) + { + FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[PEEK_BITS(w) - (leaf >> 3)]; + } + FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + *dst = g_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + FLUSH_BITS(lsb ? 1 : 0); + } + CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } + } + + for (np = 1 - big_val_cnt;; dst += 4) + { + const uint8_t *codebook_count1 = (gr_info->count1_table) ? tab33 : tab32; + int leaf = codebook_count1[PEEK_BITS(4)]; + if (!(leaf & 8)) + { + leaf = codebook_count1[(leaf >> 3) + (bs_cache << 4 >> (32 - (leaf & 3)))]; + } + FLUSH_BITS(leaf & 7); + if (BSPOS > layer3gr_limit) + { + break; + } +#define RELOAD_SCALEFACTOR if (!--np) { np = *sfb++/2; if (!np) break; one = *scf++; } +#define DEQ_COUNT1(s) if (leaf & (128 >> s)) { dst[s] = ((int32_t)bs_cache < 0) ? -one : one; FLUSH_BITS(1) } + RELOAD_SCALEFACTOR; + DEQ_COUNT1(0); + DEQ_COUNT1(1); + RELOAD_SCALEFACTOR; + DEQ_COUNT1(2); + DEQ_COUNT1(3); + CHECK_BITS; + } + + bs->pos = layer3gr_limit; +} + +static void L3_midside_stereo(float *left, int n) +{ + int i = 0; + float *right = left + 576; +#if HAVE_SIMD + if (have_simd()) + { + for (; i < n - 3; i += 4) + { + f4 vl = VLD(left + i); + f4 vr = VLD(right + i); + VSTORE(left + i, VADD(vl, vr)); + VSTORE(right + i, VSUB(vl, vr)); + } +#ifdef __GNUC__ + /* Workaround for spurious -Waggressive-loop-optimizations warning from gcc. + * For more info see: https://github.com/lieff/minimp3/issues/88 + */ + if (__builtin_constant_p(n % 4 == 0) && n % 4 == 0) + return; +#endif + } +#endif /* HAVE_SIMD */ + for (; i < n; i++) + { + float a = left[i]; + float b = right[i]; + left[i] = a + b; + right[i] = a - b; + } +} + +static void L3_intensity_stereo_band(float *left, int n, float kl, float kr) +{ + int i; + for (i = 0; i < n; i++) + { + left[i + 576] = left[i]*kr; + left[i] = left[i]*kl; + } +} + +static void L3_stereo_top_band(const float *right, const uint8_t *sfb, int nbands, int max_band[3]) +{ + int i, k; + + max_band[0] = max_band[1] = max_band[2] = -1; + + for (i = 0; i < nbands; i++) + { + for (k = 0; k < sfb[i]; k += 2) + { + if (right[k] != 0 || right[k + 1] != 0) + { + max_band[i % 3] = i; + break; + } + } + right += sfb[i]; + } +} + +static void L3_stereo_process(float *left, const uint8_t *ist_pos, const uint8_t *sfb, const uint8_t *hdr, int max_band[3], int mpeg2_sh) +{ + static const float g_pan[7*2] = { 0,1,0.21132487f,0.78867513f,0.36602540f,0.63397460f,0.5f,0.5f,0.63397460f,0.36602540f,0.78867513f,0.21132487f,1,0 }; + unsigned i, max_pos = HDR_TEST_MPEG1(hdr) ? 7 : 64; + + for (i = 0; sfb[i]; i++) + { + unsigned ipos = ist_pos[i]; + if ((int)i > max_band[i % 3] && ipos < max_pos) + { + float kl, kr, s = HDR_TEST_MS_STEREO(hdr) ? 1.41421356f : 1; + if (HDR_TEST_MPEG1(hdr)) + { + kl = g_pan[2*ipos]; + kr = g_pan[2*ipos + 1]; + } else + { + kl = 1; + kr = L3_ldexp_q2(1, (ipos + 1) >> 1 << mpeg2_sh); + if (ipos & 1) + { + kl = kr; + kr = 1; + } + } + L3_intensity_stereo_band(left, sfb[i], kl*s, kr*s); + } else if (HDR_TEST_MS_STEREO(hdr)) + { + L3_midside_stereo(left, sfb[i]); + } + left += sfb[i]; + } +} + +static void L3_intensity_stereo(float *left, uint8_t *ist_pos, const L3_gr_info_t *gr, const uint8_t *hdr) +{ + int max_band[3], n_sfb = gr->n_long_sfb + gr->n_short_sfb; + int i, max_blocks = gr->n_short_sfb ? 3 : 1; + + L3_stereo_top_band(left + 576, gr->sfbtab, n_sfb, max_band); + if (gr->n_long_sfb) + { + max_band[0] = max_band[1] = max_band[2] = MINIMP3_MAX(MINIMP3_MAX(max_band[0], max_band[1]), max_band[2]); + } + for (i = 0; i < max_blocks; i++) + { + int default_pos = HDR_TEST_MPEG1(hdr) ? 3 : 0; + int itop = n_sfb - max_blocks + i; + int prev = itop - max_blocks; + ist_pos[itop] = max_band[i] >= prev ? default_pos : ist_pos[prev]; + } + L3_stereo_process(left, ist_pos, gr->sfbtab, hdr, max_band, gr[1].scalefac_compress & 1); +} + +static void L3_reorder(float *grbuf, float *scratch, const uint8_t *sfb) +{ + int i, len; + float *src = grbuf, *dst = scratch; + + for (;0 != (len = *sfb); sfb += 3, src += 2*len) + { + for (i = 0; i < len; i++, src++) + { + *dst++ = src[0*len]; + *dst++ = src[1*len]; + *dst++ = src[2*len]; + } + } + memcpy(grbuf, scratch, (dst - scratch)*sizeof(float)); +} + +static void L3_antialias(float *grbuf, int nbands) +{ + static const float g_aa[2][8] = { + {0.85749293f,0.88174200f,0.94962865f,0.98331459f,0.99551782f,0.99916056f,0.99989920f,0.99999316f}, + {0.51449576f,0.47173197f,0.31337745f,0.18191320f,0.09457419f,0.04096558f,0.01419856f,0.00369997f} + }; + + for (; nbands > 0; nbands--, grbuf += 18) + { + int i = 0; +#if HAVE_SIMD + if (have_simd()) for (; i < 8; i += 4) + { + f4 vu = VLD(grbuf + 18 + i); + f4 vd = VLD(grbuf + 14 - i); + f4 vc0 = VLD(g_aa[0] + i); + f4 vc1 = VLD(g_aa[1] + i); + vd = VREV(vd); + VSTORE(grbuf + 18 + i, VSUB(VMUL(vu, vc0), VMUL(vd, vc1))); + vd = VADD(VMUL(vu, vc1), VMUL(vd, vc0)); + VSTORE(grbuf + 14 - i, VREV(vd)); + } +#endif /* HAVE_SIMD */ +#ifndef MINIMP3_ONLY_SIMD + for(; i < 8; i++) + { + float u = grbuf[18 + i]; + float d = grbuf[17 - i]; + grbuf[18 + i] = u*g_aa[0][i] - d*g_aa[1][i]; + grbuf[17 - i] = u*g_aa[1][i] + d*g_aa[0][i]; + } +#endif /* MINIMP3_ONLY_SIMD */ + } +} + +static void L3_dct3_9(float *y) +{ + float s0, s1, s2, s3, s4, s5, s6, s7, s8, t0, t2, t4; + + s0 = y[0]; s2 = y[2]; s4 = y[4]; s6 = y[6]; s8 = y[8]; + t0 = s0 + s6*0.5f; + s0 -= s6; + t4 = (s4 + s2)*0.93969262f; + t2 = (s8 + s2)*0.76604444f; + s6 = (s4 - s8)*0.17364818f; + s4 += s8 - s2; + + s2 = s0 - s4*0.5f; + y[4] = s4 + s0; + s8 = t0 - t2 + s6; + s0 = t0 - t4 + t2; + s4 = t0 + t4 - s6; + + s1 = y[1]; s3 = y[3]; s5 = y[5]; s7 = y[7]; + + s3 *= 0.86602540f; + t0 = (s5 + s1)*0.98480775f; + t4 = (s5 - s7)*0.34202014f; + t2 = (s1 + s7)*0.64278761f; + s1 = (s1 - s5 - s7)*0.86602540f; + + s5 = t0 - s3 - t2; + s7 = t4 - s3 - t0; + s3 = t4 + s3 - t2; + + y[0] = s4 - s7; + y[1] = s2 + s1; + y[2] = s0 - s3; + y[3] = s8 + s5; + y[5] = s8 - s5; + y[6] = s0 + s3; + y[7] = s2 - s1; + y[8] = s4 + s7; +} + +static void L3_imdct36(float *grbuf, float *overlap, const float *window, int nbands) +{ + int i, j; + static const float g_twid9[18] = { + 0.73727734f,0.79335334f,0.84339145f,0.88701083f,0.92387953f,0.95371695f,0.97629601f,0.99144486f,0.99904822f,0.67559021f,0.60876143f,0.53729961f,0.46174861f,0.38268343f,0.30070580f,0.21643961f,0.13052619f,0.04361938f + }; + + for (j = 0; j < nbands; j++, grbuf += 18, overlap += 9) + { + float co[9], si[9]; + co[0] = -grbuf[0]; + si[0] = grbuf[17]; + for (i = 0; i < 4; i++) + { + si[8 - 2*i] = grbuf[4*i + 1] - grbuf[4*i + 2]; + co[1 + 2*i] = grbuf[4*i + 1] + grbuf[4*i + 2]; + si[7 - 2*i] = grbuf[4*i + 4] - grbuf[4*i + 3]; + co[2 + 2*i] = -(grbuf[4*i + 3] + grbuf[4*i + 4]); + } + L3_dct3_9(co); + L3_dct3_9(si); + + si[1] = -si[1]; + si[3] = -si[3]; + si[5] = -si[5]; + si[7] = -si[7]; + + i = 0; + +#if HAVE_SIMD + if (have_simd()) for (; i < 8; i += 4) + { + f4 vovl = VLD(overlap + i); + f4 vc = VLD(co + i); + f4 vs = VLD(si + i); + f4 vr0 = VLD(g_twid9 + i); + f4 vr1 = VLD(g_twid9 + 9 + i); + f4 vw0 = VLD(window + i); + f4 vw1 = VLD(window + 9 + i); + f4 vsum = VADD(VMUL(vc, vr1), VMUL(vs, vr0)); + VSTORE(overlap + i, VSUB(VMUL(vc, vr0), VMUL(vs, vr1))); + VSTORE(grbuf + i, VSUB(VMUL(vovl, vw0), VMUL(vsum, vw1))); + vsum = VADD(VMUL(vovl, vw1), VMUL(vsum, vw0)); + VSTORE(grbuf + 14 - i, VREV(vsum)); + } +#endif /* HAVE_SIMD */ + for (; i < 9; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid9[9 + i] + si[i]*g_twid9[0 + i]; + overlap[i] = co[i]*g_twid9[0 + i] - si[i]*g_twid9[9 + i]; + grbuf[i] = ovl*window[0 + i] - sum*window[9 + i]; + grbuf[17 - i] = ovl*window[9 + i] + sum*window[0 + i]; + } + } +} + +static void L3_idct3(float x0, float x1, float x2, float *dst) +{ + float m1 = x1*0.86602540f; + float a1 = x0 - x2*0.5f; + dst[1] = x0 + x2; + dst[0] = a1 + m1; + dst[2] = a1 - m1; +} + +static void L3_imdct12(float *x, float *dst, float *overlap) +{ + static const float g_twid3[6] = { 0.79335334f,0.92387953f,0.99144486f, 0.60876143f,0.38268343f,0.13052619f }; + float co[3], si[3]; + int i; + + L3_idct3(-x[0], x[6] + x[3], x[12] + x[9], co); + L3_idct3(x[15], x[12] - x[9], x[6] - x[3], si); + si[1] = -si[1]; + + for (i = 0; i < 3; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid3[3 + i] + si[i]*g_twid3[0 + i]; + overlap[i] = co[i]*g_twid3[0 + i] - si[i]*g_twid3[3 + i]; + dst[i] = ovl*g_twid3[2 - i] - sum*g_twid3[5 - i]; + dst[5 - i] = ovl*g_twid3[5 - i] + sum*g_twid3[2 - i]; + } +} + +static void L3_imdct_short(float *grbuf, float *overlap, int nbands) +{ + for (;nbands > 0; nbands--, overlap += 9, grbuf += 18) + { + float tmp[18]; + memcpy(tmp, grbuf, sizeof(tmp)); + memcpy(grbuf, overlap, 6*sizeof(float)); + L3_imdct12(tmp, grbuf + 6, overlap + 6); + L3_imdct12(tmp + 1, grbuf + 12, overlap + 6); + L3_imdct12(tmp + 2, overlap, overlap + 6); + } +} + +static void L3_change_sign(float *grbuf) +{ + int b, i; + for (b = 0, grbuf += 18; b < 32; b += 2, grbuf += 36) + for (i = 1; i < 18; i += 2) + grbuf[i] = -grbuf[i]; +} + +static void L3_imdct_gr(float *grbuf, float *overlap, unsigned block_type, unsigned n_long_bands) +{ + static const float g_mdct_window[2][18] = { + { 0.99904822f,0.99144486f,0.97629601f,0.95371695f,0.92387953f,0.88701083f,0.84339145f,0.79335334f,0.73727734f,0.04361938f,0.13052619f,0.21643961f,0.30070580f,0.38268343f,0.46174861f,0.53729961f,0.60876143f,0.67559021f }, + { 1,1,1,1,1,1,0.99144486f,0.92387953f,0.79335334f,0,0,0,0,0,0,0.13052619f,0.38268343f,0.60876143f } + }; + if (n_long_bands) + { + L3_imdct36(grbuf, overlap, g_mdct_window[0], n_long_bands); + grbuf += 18*n_long_bands; + overlap += 9*n_long_bands; + } + if (block_type == SHORT_BLOCK_TYPE) + L3_imdct_short(grbuf, overlap, 32 - n_long_bands); + else + L3_imdct36(grbuf, overlap, g_mdct_window[block_type == STOP_BLOCK_TYPE], 32 - n_long_bands); +} + +static void L3_save_reservoir(mp3dec_t *h, mp3dec_scratch_t *s) +{ + int pos = (s->bs.pos + 7)/8u; + int remains = s->bs.limit/8u - pos; + if (remains > MAX_BITRESERVOIR_BYTES) + { + pos += remains - MAX_BITRESERVOIR_BYTES; + remains = MAX_BITRESERVOIR_BYTES; + } + if (remains > 0) + { + memmove(h->reserv_buf, s->maindata + pos, remains); + } + h->reserv = remains; +} + +static int L3_restore_reservoir(mp3dec_t *h, bs_t *bs, mp3dec_scratch_t *s, int main_data_begin) +{ + int frame_bytes = (bs->limit - bs->pos)/8; + int bytes_have = MINIMP3_MIN(h->reserv, main_data_begin); + memcpy(s->maindata, h->reserv_buf + MINIMP3_MAX(0, h->reserv - main_data_begin), MINIMP3_MIN(h->reserv, main_data_begin)); + memcpy(s->maindata + bytes_have, bs->buf + bs->pos/8, frame_bytes); + bs_init(&s->bs, s->maindata, bytes_have + frame_bytes); + return h->reserv >= main_data_begin; +} + +static void L3_decode(mp3dec_t *h, mp3dec_scratch_t *s, L3_gr_info_t *gr_info, int nch) +{ + int ch; + + for (ch = 0; ch < nch; ch++) + { + int layer3gr_limit = s->bs.pos + gr_info[ch].part_23_length; + L3_decode_scalefactors(h->header, s->ist_pos[ch], &s->bs, gr_info + ch, s->scf, ch); + L3_huffman(s->grbuf[ch], &s->bs, gr_info + ch, s->scf, layer3gr_limit); + } + + if (HDR_TEST_I_STEREO(h->header)) + { + L3_intensity_stereo(s->grbuf[0], s->ist_pos[1], gr_info, h->header); + } else if (HDR_IS_MS_STEREO(h->header)) + { + L3_midside_stereo(s->grbuf[0], 576); + } + + for (ch = 0; ch < nch; ch++, gr_info++) + { + int aa_bands = 31; + int n_long_bands = (gr_info->mixed_block_flag ? 2 : 0) << (int)(HDR_GET_MY_SAMPLE_RATE(h->header) == 2); + + if (gr_info->n_short_sfb) + { + aa_bands = n_long_bands - 1; + L3_reorder(s->grbuf[ch] + n_long_bands*18, s->syn[0], gr_info->sfbtab + gr_info->n_long_sfb); + } + + L3_antialias(s->grbuf[ch], aa_bands); + L3_imdct_gr(s->grbuf[ch], h->mdct_overlap[ch], gr_info->block_type, n_long_bands); + L3_change_sign(s->grbuf[ch]); + } +} + +static void mp3d_DCT_II(float *grbuf, int n) +{ + static const float g_sec[24] = { + 10.19000816f,0.50060302f,0.50241929f,3.40760851f,0.50547093f,0.52249861f,2.05778098f,0.51544732f,0.56694406f,1.48416460f,0.53104258f,0.64682180f,1.16943991f,0.55310392f,0.78815460f,0.97256821f,0.58293498f,1.06067765f,0.83934963f,0.62250412f,1.72244716f,0.74453628f,0.67480832f,5.10114861f + }; + int i, k = 0; +#if HAVE_SIMD + if (have_simd()) for (; k < n; k += 4) + { + f4 t[4][8], *x; + float *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + f4 x0 = VLD(&y[i*18]); + f4 x1 = VLD(&y[(15 - i)*18]); + f4 x2 = VLD(&y[(16 + i)*18]); + f4 x3 = VLD(&y[(31 - i)*18]); + f4 t0 = VADD(x0, x3); + f4 t1 = VADD(x1, x2); + f4 t2 = VMUL_S(VSUB(x1, x2), g_sec[3*i + 0]); + f4 t3 = VMUL_S(VSUB(x0, x3), g_sec[3*i + 1]); + x[0] = VADD(t0, t1); + x[8] = VMUL_S(VSUB(t0, t1), g_sec[3*i + 2]); + x[16] = VADD(t3, t2); + x[24] = VMUL_S(VSUB(t3, t2), g_sec[3*i + 2]); + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + f4 x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = VSUB(x0, x7); x0 = VADD(x0, x7); + x7 = VSUB(x1, x6); x1 = VADD(x1, x6); + x6 = VSUB(x2, x5); x2 = VADD(x2, x5); + x5 = VSUB(x3, x4); x3 = VADD(x3, x4); + x4 = VSUB(x0, x3); x0 = VADD(x0, x3); + x3 = VSUB(x1, x2); x1 = VADD(x1, x2); + x[0] = VADD(x0, x1); + x[4] = VMUL_S(VSUB(x0, x1), 0.70710677f); + x5 = VADD(x5, x6); + x6 = VMUL_S(VADD(x6, x7), 0.70710677f); + x7 = VADD(x7, xt); + x3 = VMUL_S(VADD(x3, x4), 0.70710677f); + x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); /* rotate by PI/8 */ + x7 = VADD(x7, VMUL_S(x5, 0.382683432f)); + x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); + x0 = VSUB(xt, x6); xt = VADD(xt, x6); + x[1] = VMUL_S(VADD(xt, x7), 0.50979561f); + x[2] = VMUL_S(VADD(x4, x3), 0.54119611f); + x[3] = VMUL_S(VSUB(x0, x5), 0.60134488f); + x[5] = VMUL_S(VADD(x0, x5), 0.89997619f); + x[6] = VMUL_S(VSUB(x4, x3), 1.30656302f); + x[7] = VMUL_S(VSUB(xt, x7), 2.56291556f); + } + + if (k > n - 3) + { +#if HAVE_SSE +#define VSAVE2(i, v) _mm_storel_pi((__m64 *)(void*)&y[i*18], v) +#else /* HAVE_SSE */ +#define VSAVE2(i, v) vst1_f32((float32_t *)&y[i*18], vget_low_f32(v)) +#endif /* HAVE_SSE */ + for (i = 0; i < 7; i++, y += 4*18) + { + f4 s = VADD(t[3][i], t[3][i + 1]); + VSAVE2(0, t[0][i]); + VSAVE2(1, VADD(t[2][i], s)); + VSAVE2(2, VADD(t[1][i], t[1][i + 1])); + VSAVE2(3, VADD(t[2][1 + i], s)); + } + VSAVE2(0, t[0][7]); + VSAVE2(1, VADD(t[2][7], t[3][7])); + VSAVE2(2, t[1][7]); + VSAVE2(3, t[3][7]); + } else + { +#define VSAVE4(i, v) VSTORE(&y[i*18], v) + for (i = 0; i < 7; i++, y += 4*18) + { + f4 s = VADD(t[3][i], t[3][i + 1]); + VSAVE4(0, t[0][i]); + VSAVE4(1, VADD(t[2][i], s)); + VSAVE4(2, VADD(t[1][i], t[1][i + 1])); + VSAVE4(3, VADD(t[2][1 + i], s)); + } + VSAVE4(0, t[0][7]); + VSAVE4(1, VADD(t[2][7], t[3][7])); + VSAVE4(2, t[1][7]); + VSAVE4(3, t[3][7]); + } + } else +#endif /* HAVE_SIMD */ +#ifdef MINIMP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else /* MINIMP3_ONLY_SIMD */ + for (; k < n; k++) + { + float t[4][8], *x, *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + float x0 = y[i*18]; + float x1 = y[(15 - i)*18]; + float x2 = y[(16 + i)*18]; + float x3 = y[(31 - i)*18]; + float t0 = x0 + x3; + float t1 = x1 + x2; + float t2 = (x1 - x2)*g_sec[3*i + 0]; + float t3 = (x0 - x3)*g_sec[3*i + 1]; + x[0] = t0 + t1; + x[8] = (t0 - t1)*g_sec[3*i + 2]; + x[16] = t3 + t2; + x[24] = (t3 - t2)*g_sec[3*i + 2]; + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + float x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = x0 - x7; x0 += x7; + x7 = x1 - x6; x1 += x6; + x6 = x2 - x5; x2 += x5; + x5 = x3 - x4; x3 += x4; + x4 = x0 - x3; x0 += x3; + x3 = x1 - x2; x1 += x2; + x[0] = x0 + x1; + x[4] = (x0 - x1)*0.70710677f; + x5 = x5 + x6; + x6 = (x6 + x7)*0.70710677f; + x7 = x7 + xt; + x3 = (x3 + x4)*0.70710677f; + x5 -= x7*0.198912367f; /* rotate by PI/8 */ + x7 += x5*0.382683432f; + x5 -= x7*0.198912367f; + x0 = xt - x6; xt += x6; + x[1] = (xt + x7)*0.50979561f; + x[2] = (x4 + x3)*0.54119611f; + x[3] = (x0 - x5)*0.60134488f; + x[5] = (x0 + x5)*0.89997619f; + x[6] = (x4 - x3)*1.30656302f; + x[7] = (xt - x7)*2.56291556f; + + } + for (i = 0; i < 7; i++, y += 4*18) + { + y[0*18] = t[0][i]; + y[1*18] = t[2][i] + t[3][i] + t[3][i + 1]; + y[2*18] = t[1][i] + t[1][i + 1]; + y[3*18] = t[2][i + 1] + t[3][i] + t[3][i + 1]; + } + y[0*18] = t[0][7]; + y[1*18] = t[2][7] + t[3][7]; + y[2*18] = t[1][7]; + y[3*18] = t[3][7]; + } +#endif /* MINIMP3_ONLY_SIMD */ +} + +#ifndef MINIMP3_FLOAT_OUTPUT +static int16_t mp3d_scale_pcm(float sample) +{ +#if HAVE_ARMV6 + int32_t s32 = (int32_t)(sample + .5f); + s32 -= (s32 < 0); + int16_t s = (int16_t)minimp3_clip_int16_arm(s32); +#else + if (sample >= 32766.5) return (int16_t) 32767; + if (sample <= -32767.5) return (int16_t)-32768; + int16_t s = (int16_t)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ +#endif + return s; +} +#else /* MINIMP3_FLOAT_OUTPUT */ +static float mp3d_scale_pcm(float sample) +{ + return sample*(1.f/32768.f); +} +#endif /* MINIMP3_FLOAT_OUTPUT */ + +static void mp3d_synth_pair(mp3d_sample_t *pcm, int nch, const float *z) +{ + float a; + a = (z[14*64] - z[ 0]) * 29; + a += (z[ 1*64] + z[13*64]) * 213; + a += (z[12*64] - z[ 2*64]) * 459; + a += (z[ 3*64] + z[11*64]) * 2037; + a += (z[10*64] - z[ 4*64]) * 5153; + a += (z[ 5*64] + z[ 9*64]) * 6574; + a += (z[ 8*64] - z[ 6*64]) * 37489; + a += z[ 7*64] * 75038; + pcm[0] = mp3d_scale_pcm(a); + + z += 2; + a = z[14*64] * 104; + a += z[12*64] * 1567; + a += z[10*64] * 9727; + a += z[ 8*64] * 64019; + a += z[ 6*64] * -9975; + a += z[ 4*64] * -45; + a += z[ 2*64] * 146; + a += z[ 0*64] * -5; + pcm[16*nch] = mp3d_scale_pcm(a); +} + +static void mp3d_synth(float *xl, mp3d_sample_t *dstl, int nch, float *lins) +{ + int i; + float *xr = xl + 576*(nch - 1); + mp3d_sample_t *dstr = dstl + (nch - 1); + + static const float g_win[] = { + -1,26,-31,208,218,401,-519,2063,2000,4788,-5517,7134,5959,35640,-39336,74992, + -1,24,-35,202,222,347,-581,2080,1952,4425,-5879,7640,5288,33791,-41176,74856, + -1,21,-38,196,225,294,-645,2087,1893,4063,-6237,8092,4561,31947,-43006,74630, + -1,19,-41,190,227,244,-711,2085,1822,3705,-6589,8492,3776,30112,-44821,74313, + -1,17,-45,183,228,197,-779,2075,1739,3351,-6935,8840,2935,28289,-46617,73908, + -1,16,-49,176,228,153,-848,2057,1644,3004,-7271,9139,2037,26482,-48390,73415, + -2,14,-53,169,227,111,-919,2032,1535,2663,-7597,9389,1082,24694,-50137,72835, + -2,13,-58,161,224,72,-991,2001,1414,2330,-7910,9592,70,22929,-51853,72169, + -2,11,-63,154,221,36,-1064,1962,1280,2006,-8209,9750,-998,21189,-53534,71420, + -2,10,-68,147,215,2,-1137,1919,1131,1692,-8491,9863,-2122,19478,-55178,70590, + -3,9,-73,139,208,-29,-1210,1870,970,1388,-8755,9935,-3300,17799,-56778,69679, + -3,8,-79,132,200,-57,-1283,1817,794,1095,-8998,9966,-4533,16155,-58333,68692, + -4,7,-85,125,189,-83,-1356,1759,605,814,-9219,9959,-5818,14548,-59838,67629, + -4,7,-91,117,177,-106,-1428,1698,402,545,-9416,9916,-7154,12980,-61289,66494, + -5,6,-97,111,163,-127,-1498,1634,185,288,-9585,9838,-8540,11455,-62684,65290 + }; + float *zlin = lins + 15*64; + const float *w = g_win; + + zlin[4*15] = xl[18*16]; + zlin[4*15 + 1] = xr[18*16]; + zlin[4*15 + 2] = xl[0]; + zlin[4*15 + 3] = xr[0]; + + zlin[4*31] = xl[1 + 18*16]; + zlin[4*31 + 1] = xr[1 + 18*16]; + zlin[4*31 + 2] = xl[1]; + zlin[4*31 + 3] = xr[1]; + + mp3d_synth_pair(dstr, nch, lins + 4*15 + 1); + mp3d_synth_pair(dstr + 32*nch, nch, lins + 4*15 + 64 + 1); + mp3d_synth_pair(dstl, nch, lins + 4*15); + mp3d_synth_pair(dstl + 32*nch, nch, lins + 4*15 + 64); + +#if HAVE_SIMD + if (have_simd()) for (i = 14; i >= 0; i--) + { +#define VLOAD(k) f4 w0 = VSET(*w++); f4 w1 = VSET(*w++); f4 vz = VLD(&zlin[4*i - 64*k]); f4 vy = VLD(&zlin[4*i - 64*(15 - k)]); +#define V0(k) { VLOAD(k) b = VADD(VMUL(vz, w1), VMUL(vy, w0)) ; a = VSUB(VMUL(vz, w0), VMUL(vy, w1)); } +#define V1(k) { VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); a = VADD(a, VSUB(VMUL(vz, w0), VMUL(vy, w1))); } +#define V2(k) { VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); a = VADD(a, VSUB(VMUL(vy, w1), VMUL(vz, w0))); } + f4 a, b; + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*i + 64] = xl[1 + 18*(1 + i)]; + zlin[4*i + 64 + 1] = xr[1 + 18*(1 + i)]; + zlin[4*i - 64 + 2] = xl[18*(1 + i)]; + zlin[4*i - 64 + 3] = xr[18*(1 + i)]; + + V0(0) V2(1) V1(2) V2(3) V1(4) V2(5) V1(6) V2(7) + + { +#ifndef MINIMP3_FLOAT_OUTPUT +#if HAVE_SSE + static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + dstr[(15 - i)*nch] = _mm_extract_epi16(pcm8, 1); + dstr[(17 + i)*nch] = _mm_extract_epi16(pcm8, 5); + dstl[(15 - i)*nch] = _mm_extract_epi16(pcm8, 0); + dstl[(17 + i)*nch] = _mm_extract_epi16(pcm8, 4); + dstr[(47 - i)*nch] = _mm_extract_epi16(pcm8, 3); + dstr[(49 + i)*nch] = _mm_extract_epi16(pcm8, 7); + dstl[(47 - i)*nch] = _mm_extract_epi16(pcm8, 2); + dstl[(49 + i)*nch] = _mm_extract_epi16(pcm8, 6); +#else /* HAVE_SSE */ + int16x4_t pcma, pcmb; + a = VADD(a, VSET(0.5f)); + b = VADD(b, VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, VSET(0))))); + vst1_lane_s16(dstr + (15 - i)*nch, pcma, 1); + vst1_lane_s16(dstr + (17 + i)*nch, pcmb, 1); + vst1_lane_s16(dstl + (15 - i)*nch, pcma, 0); + vst1_lane_s16(dstl + (17 + i)*nch, pcmb, 0); + vst1_lane_s16(dstr + (47 - i)*nch, pcma, 3); + vst1_lane_s16(dstr + (49 + i)*nch, pcmb, 3); + vst1_lane_s16(dstl + (47 - i)*nch, pcma, 2); + vst1_lane_s16(dstl + (49 + i)*nch, pcmb, 2); +#endif /* HAVE_SSE */ + +#else /* MINIMP3_FLOAT_OUTPUT */ + + static const f4 g_scale = { 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f }; + a = VMUL(a, g_scale); + b = VMUL(b, g_scale); +#if HAVE_SSE + _mm_store_ss(dstr + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstr + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstl + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstl + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstr + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstr + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstl + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(2, 2, 2, 2))); + _mm_store_ss(dstl + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(2, 2, 2, 2))); +#else /* HAVE_SSE */ + vst1q_lane_f32(dstr + (15 - i)*nch, a, 1); + vst1q_lane_f32(dstr + (17 + i)*nch, b, 1); + vst1q_lane_f32(dstl + (15 - i)*nch, a, 0); + vst1q_lane_f32(dstl + (17 + i)*nch, b, 0); + vst1q_lane_f32(dstr + (47 - i)*nch, a, 3); + vst1q_lane_f32(dstr + (49 + i)*nch, b, 3); + vst1q_lane_f32(dstl + (47 - i)*nch, a, 2); + vst1q_lane_f32(dstl + (49 + i)*nch, b, 2); +#endif /* HAVE_SSE */ +#endif /* MINIMP3_FLOAT_OUTPUT */ + } + } else +#endif /* HAVE_SIMD */ +#ifdef MINIMP3_ONLY_SIMD + {} /* for HAVE_SIMD=1, MINIMP3_ONLY_SIMD=1 case we do not need non-intrinsic "else" branch */ +#else /* MINIMP3_ONLY_SIMD */ + for (i = 14; i >= 0; i--) + { +#define LOAD(k) float w0 = *w++; float w1 = *w++; float *vz = &zlin[4*i - k*64]; float *vy = &zlin[4*i - (15 - k)*64]; +#define S0(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] = vz[j]*w1 + vy[j]*w0, a[j] = vz[j]*w0 - vy[j]*w1; } +#define S1(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vz[j]*w0 - vy[j]*w1; } +#define S2(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vy[j]*w1 - vz[j]*w0; } + float a[4], b[4]; + + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*(i + 16)] = xl[1 + 18*(1 + i)]; + zlin[4*(i + 16) + 1] = xr[1 + 18*(1 + i)]; + zlin[4*(i - 16) + 2] = xl[18*(1 + i)]; + zlin[4*(i - 16) + 3] = xr[18*(1 + i)]; + + S0(0) S2(1) S1(2) S2(3) S1(4) S2(5) S1(6) S2(7) + + dstr[(15 - i)*nch] = mp3d_scale_pcm(a[1]); + dstr[(17 + i)*nch] = mp3d_scale_pcm(b[1]); + dstl[(15 - i)*nch] = mp3d_scale_pcm(a[0]); + dstl[(17 + i)*nch] = mp3d_scale_pcm(b[0]); + dstr[(47 - i)*nch] = mp3d_scale_pcm(a[3]); + dstr[(49 + i)*nch] = mp3d_scale_pcm(b[3]); + dstl[(47 - i)*nch] = mp3d_scale_pcm(a[2]); + dstl[(49 + i)*nch] = mp3d_scale_pcm(b[2]); + } +#endif /* MINIMP3_ONLY_SIMD */ +} + +static void mp3d_synth_granule(float *qmf_state, float *grbuf, int nbands, int nch, mp3d_sample_t *pcm, float *lins) +{ + int i; + for (i = 0; i < nch; i++) + { + mp3d_DCT_II(grbuf + 576*i, nbands); + } + + memcpy(lins, qmf_state, sizeof(float)*15*64); + + for (i = 0; i < nbands; i += 2) + { + mp3d_synth(grbuf + i, pcm + 32*nch*i, nch, lins + i*64); + } +#ifndef MINIMP3_NONSTANDARD_BUT_LOGICAL + if (nch == 1) + { + for (i = 0; i < 15*64; i += 2) + { + qmf_state[i] = lins[nbands*64 + i]; + } + } else +#endif /* MINIMP3_NONSTANDARD_BUT_LOGICAL */ + { + memcpy(qmf_state, lins + nbands*64, sizeof(float)*15*64); + } +} + +static int mp3d_match_frame(const uint8_t *hdr, int mp3_bytes, int frame_bytes) +{ + int i, nmatch; + for (i = 0, nmatch = 0; nmatch < MAX_FRAME_SYNC_MATCHES; nmatch++) + { + i += hdr_frame_bytes(hdr + i, frame_bytes) + hdr_padding(hdr + i); + if (i + HDR_SIZE > mp3_bytes) + return nmatch > 0; + if (!hdr_compare(hdr, hdr + i)) + return 0; + } + return 1; +} + +static int mp3d_find_frame(const uint8_t *mp3, int mp3_bytes, int *free_format_bytes, int *ptr_frame_bytes) +{ + int i, k; + for (i = 0; i < mp3_bytes - HDR_SIZE; i++, mp3++) + { + if (hdr_valid(mp3)) + { + int frame_bytes = hdr_frame_bytes(mp3, *free_format_bytes); + int frame_and_padding = frame_bytes + hdr_padding(mp3); + + for (k = HDR_SIZE; !frame_bytes && k < MAX_FREE_FORMAT_FRAME_SIZE && i + 2*k < mp3_bytes - HDR_SIZE; k++) + { + if (hdr_compare(mp3, mp3 + k)) + { + int fb = k - hdr_padding(mp3); + int nextfb = fb + hdr_padding(mp3 + k); + if (i + k + nextfb + HDR_SIZE > mp3_bytes || !hdr_compare(mp3, mp3 + k + nextfb)) + continue; + frame_and_padding = k; + frame_bytes = fb; + *free_format_bytes = fb; + } + } + if ((frame_bytes && i + frame_and_padding <= mp3_bytes && + mp3d_match_frame(mp3, mp3_bytes - i, frame_bytes)) || + (!i && frame_and_padding == mp3_bytes)) + { + *ptr_frame_bytes = frame_and_padding; + return i; + } + *free_format_bytes = 0; + } + } + *ptr_frame_bytes = 0; + return mp3_bytes; +} + +void mp3dec_init(mp3dec_t *dec) +{ + dec->header[0] = 0; +} + +int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info) +{ + int i = 0, igr, frame_size = 0, success = 1; + const uint8_t *hdr; + bs_t bs_frame[1]; +#ifdef MINIMP3_SCRATCH +#define scratch (*MINIMP3_SCRATCH) +#else + mp3dec_scratch_t scratch; +#endif + + if (mp3_bytes > 4 && dec->header[0] == 0xff && hdr_compare(dec->header, mp3)) + { + frame_size = hdr_frame_bytes(mp3, dec->free_format_bytes) + hdr_padding(mp3); + if (frame_size != mp3_bytes && (frame_size + HDR_SIZE > mp3_bytes || !hdr_compare(mp3, mp3 + frame_size))) + { + frame_size = 0; + } + } + if (!frame_size) + { + memset(dec, 0, sizeof(mp3dec_t)); + i = mp3d_find_frame(mp3, mp3_bytes, &dec->free_format_bytes, &frame_size); + if (!frame_size || i + frame_size > mp3_bytes) + { + info->frame_bytes = i; + return 0; + } + } + + hdr = mp3 + i; + memcpy(dec->header, hdr, HDR_SIZE); + info->frame_bytes = i + frame_size; + info->frame_offset = i; + info->channels = HDR_IS_MONO(hdr) ? 1 : 2; + info->hz = hdr_sample_rate_hz(hdr); + info->layer = 4 - HDR_GET_LAYER(hdr); + info->bitrate_kbps = hdr_bitrate_kbps(hdr); + + if (!pcm) + { + return hdr_frame_samples(hdr); + } + + bs_init(bs_frame, hdr + HDR_SIZE, frame_size - HDR_SIZE); + if (HDR_IS_CRC(hdr)) + { + get_bits(bs_frame, 16); + } + + if (info->layer == 3) + { + int main_data_begin = L3_read_side_info(bs_frame, scratch.gr_info, hdr); + if (main_data_begin < 0 || bs_frame->pos > bs_frame->limit) + { + mp3dec_init(dec); + return 0; + } + success = L3_restore_reservoir(dec, bs_frame, &scratch, main_data_begin); + if (success) + { + for (igr = 0; igr < (HDR_TEST_MPEG1(hdr) ? 2 : 1); igr++, pcm += 576*info->channels) + { + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + L3_decode(dec, &scratch, scratch.gr_info + igr*info->channels, info->channels); + mp3d_synth_granule(dec->qmf_state, scratch.grbuf[0], 18, info->channels, pcm, scratch.syn[0]); + } + } + L3_save_reservoir(dec, &scratch); + } else + { +#ifdef MINIMP3_ONLY_MP3 + return 0; +#else /* MINIMP3_ONLY_MP3 */ + L12_scale_info sci[1]; + L12_read_scale_info(hdr, bs_frame, sci); + + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + for (i = 0, igr = 0; igr < 3; igr++) + { + if (12 == (i += L12_dequantize_granule(scratch.grbuf[0] + i, bs_frame, sci, info->layer | 1))) + { + i = 0; + L12_apply_scf_384(sci, sci->scf + igr, scratch.grbuf[0]); + mp3d_synth_granule(dec->qmf_state, scratch.grbuf[0], 12, info->channels, pcm, scratch.syn[0]); + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + pcm += 384*info->channels; + } + if (bs_frame->pos > bs_frame->limit) + { + mp3dec_init(dec); + return 0; + } + } +#endif /* MINIMP3_ONLY_MP3 */ + } +#ifdef MINIMP3_SCRATCH +#undef scratch +#endif + return success*hdr_frame_samples(dec->header); +} + +#ifdef MINIMP3_FLOAT_OUTPUT +void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples) +{ + int i = 0; +#if HAVE_SIMD + int aligned_count = num_samples & ~7; + for(; i < aligned_count; i += 8) + { + static const f4 g_scale = { 32768.0f, 32768.0f, 32768.0f, 32768.0f }; + f4 a = VMUL(VLD(&in[i ]), g_scale); + f4 b = VMUL(VLD(&in[i+4]), g_scale); +#if HAVE_SSE + static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + out[i ] = _mm_extract_epi16(pcm8, 0); + out[i+1] = _mm_extract_epi16(pcm8, 1); + out[i+2] = _mm_extract_epi16(pcm8, 2); + out[i+3] = _mm_extract_epi16(pcm8, 3); + out[i+4] = _mm_extract_epi16(pcm8, 4); + out[i+5] = _mm_extract_epi16(pcm8, 5); + out[i+6] = _mm_extract_epi16(pcm8, 6); + out[i+7] = _mm_extract_epi16(pcm8, 7); +#else /* HAVE_SSE */ + int16x4_t pcma, pcmb; + a = VADD(a, VSET(0.5f)); + b = VADD(b, VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, VSET(0))))); + vst1_lane_s16(out+i , pcma, 0); + vst1_lane_s16(out+i+1, pcma, 1); + vst1_lane_s16(out+i+2, pcma, 2); + vst1_lane_s16(out+i+3, pcma, 3); + vst1_lane_s16(out+i+4, pcmb, 0); + vst1_lane_s16(out+i+5, pcmb, 1); + vst1_lane_s16(out+i+6, pcmb, 2); + vst1_lane_s16(out+i+7, pcmb, 3); +#endif /* HAVE_SSE */ + } +#endif /* HAVE_SIMD */ + for(; i < num_samples; i++) + { + float sample = in[i] * 32768.0f; + if (sample >= 32766.5) + out[i] = (int16_t) 32767; + else if (sample <= -32767.5) + out[i] = (int16_t)-32768; + else + { + int16_t s = (int16_t)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ + out[i] = s; + } + } +} +#endif /* MINIMP3_FLOAT_OUTPUT */ +#endif /* MINIMP3_IMPLEMENTATION && !_MINIMP3_IMPLEMENTATION_GUARD */ diff --git a/platformio.ini b/platformio.ini index 0d67afa..0c51f74 100644 --- a/platformio.ini +++ b/platformio.ini @@ -613,6 +613,15 @@ build_flags = -D HAS_TDECK_GT911=1 -D HAS_TDECK_TRACKBALL=1 -D HAS_TDECK_KEYBOARD=1 + ; Forced here (not just in lv_conf.h) because .pio/libdeps//MeshCore/include/lv_conf.h + ; -- a stale snapshot vendored by the pinned core tag -- sits earlier in the -I search + ; order than this repo's own include/lv_conf.h, so LV_CONF_INCLUDE_SIMPLE's quoted + ; #include "lv_conf.h" silently resolves to that stale copy for src/ compiles (though + ; library .c files, e.g. lvgl's own, still see the right one -- inconsistent per TU). A + ; -D here is immune to that: it's set before any header is processed and isn't + ; overridden by a plain #define. Only 20px is forced, for the "at a glance" experiment + ; below (atGlanceEnsureFont() in UITask.cpp) -- not 18/24, which this board doesn't use. + -D LV_FONT_MONTSERRAT_20=1 -D ENABLE_ADVERT_ON_BOOT=0 -D PIN_TB_UP=15 -D PIN_TB_DOWN=3 @@ -1143,13 +1152,17 @@ board_build.partitions = variants/thinknode_m9/partitions_m9_touch.csv ; DriveDiosInSleepMode (0x012A, FW 0x0308+) with CMD_PERR -> begin() = -706. ; The pre-script makes RadioLib's config() skip it on old FW (idempotent, ; per-env libdeps copy). See scripts/build/patch_radiolib_lr11x0.py. -extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_radiolib_lr11x0.py, merge-bin.py +extra_scripts = pre:scripts/build/pre_gen_baked.py, pre:scripts/inject_wifi_env.py, pre:scripts/build/patch_radiolib_lr11x0.py, pre:scripts/build/patch_meshcore_rv3028_define.py, merge-bin.py + +build_unflags = + -std=gnu++11 + -std=gnu++14 build_flags = ; 1.17 core: ESP32Board.cpp includes -> MicroNMEALocationProvider.h (header-only ; provider), so MicroNMEA never chains into the core lib's LDF scope -- hand it the path. -I"${platformio.libdeps_dir}/${this.__env__}/MicroNMEA/src" - -Wno-deprecated-declarations -Wno-unused-parameter -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 + -Wno-deprecated-declarations -Wno-unused-parameter -Wno-cpp -Wno-overflow -Wno-return-type -std=gnu++17 -DNDEBUG -DRADIOLIB_STATIC_ONLY=1 -DRADIOLIB_GODMODE=1 -D MC_VENDORED_TOUCH_APP -D LORA_FREQ=869.618 -D LORA_BW=62.5 @@ -1331,9 +1344,8 @@ build_flags = -D HAS_TOUCH_UI=1 -D FIRMWARE_OTA_ENV='"ThinkNode_M9_companion_radio_touch"' -D ENABLE_ADVERT_ON_BOOT=0 - -D LV_CONF_PATH=lv_conf.h + -D LV_CONF_PATH=lv_conf_m9.h -D LV_CONF_INCLUDE_SIMPLE=1 - -D LV_INDEV_DEF_SCROLL_THROW=7 -D MAX_CONTACTS=2000 -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=ST7789LCDDisplay diff --git a/release-notes/beta_70.txt b/release-notes/beta_70.txt new file mode 100644 index 0000000..c713bbc --- /dev/null +++ b/release-notes/beta_70.txt @@ -0,0 +1,39 @@ +# beta_70 - test build. Stable stays beta_65. +# One user-facing note per non-blank, non-# line; # lines are section comments. + +# --- The crash on the regions screen ------------------------------------------------------ +Adding or removing known regions and then leaving that screen crashed the device. If you have been unable to get out of Radio & mesh without resetting, this was why. +The cause was general, not specific to regions: when a text field is destroyed, the interface library sends it one last "you lost focus" event, and seven fields in the firmware save their value on losing focus. So a field was reading and rewriting itself from inside its own destructor. All seven now ignore a blur that is really a deletion. +Found by decoding the core dump attached to the report, against a rebuilt beta_69 image. Thank you for sending it. + +# --- Translations: undoing damage we caused ------------------------------------------------ +Language file v18 contained keys that do not exist anywhere in the firmware. Entries like "Paste (move)Paste (copy)" and "Unblock Block" were two separate buttons glued into one, and one entry was a Dutch phrase that appears only inside a source-code comment. +That was a bug in the tool that decides which text belongs in a language file, introduced in the last release. It has been fixed twice over: it no longer joins the two halves of a choice into one entry, and it no longer reads text out of comments. +202 invented entries are gone from all thirteen files. Nothing a translator had actually translated was touched. +The curly quotes in the Hungarian map credits showed as empty boxes because no bundled font contains them. Replaced with straight quotes. +Five more strings are translatable: the Wi-Fi rescan and hidden-network rows, the update check, and the downgrade prompt. +Language files are at v19. v18 is withdrawn. + +# --- The Store stops offering apps your board cannot run ----------------------------------- +Wardrive and Nearby need the extended SDK, which the Heltec V4 does not have. The Store offered both anyway, so the most common board in the mesh could download, install and open two apps that then refused to work, with no warning. Apps can now say what they need, and the Store says so on the row instead. + +# --- Community work, by Michael A. Cojocari (@oumike) --------------------------------------- +Lua apps can play WAV and MP3 audio, from the SD card or from memory. +@-mention autocomplete in the device chat and the web interface, suggesting people whose adverts this device actually heard rather than everyone in your contacts. +Symbol and alt keys latch: tap once for the next key, tap twice to lock the layer until you tap again. No more holding a modifier down. +The web reader opens a home page from the SD card if you put one there, so a device with no internet still has somewhere to start. +Wardrive no longer stops when a node name contains an emoji. Names are cut on character boundaries now, and text coming from an app is checked before it reaches the screen. +Screenshots from the Control Center on the ThinkNode M9 and both T-Lora Pagers, with a timer for immediate, 3-second or 10-second capture. +The M9's Home key works while you are typing in a text field, and the built-in Snake starts from the first direction you press. +Tapping a chat composer that is already focused reopens the on-screen keyboard instead of doing nothing. + +# --- Community work, by Tesso M Costa (@codemonkeybr) -------------------------------------- +A new "At a glance" notification style: when a message arrives while the screen is dimmed, it shows a short preview without fully waking the device or lighting the keyboard. Optional, and it can be limited to when the device is unlocked. + +# --- Also fixed ----------------------------------------------------------------------------- +Granting an app permission could silently take a permission away from a different app. The switches packed the app's position and the permission into too few bits, so "Send discovery probes" wrote the wrong app's record, marking an app you never touched as refused. That is why apps reported "permission denied" with every permission granted. + +# --- For testers ------------------------------------------------------------------------------ +The regions crash is the fix most worth confirming: add and remove a few known regions, leave the screen, and check the device stays up. +If you translate, your language file is clean again but has more untranslated entries than before, because strings that were previously invisible to the checker are now listed. +Console mode is still an early preview and has not changed this release. diff --git a/release-notes/beta_71.txt b/release-notes/beta_71.txt new file mode 100644 index 0000000..1ddb87d --- /dev/null +++ b/release-notes/beta_71.txt @@ -0,0 +1,14 @@ +# beta_71 - test build. Stable stays beta_65. +# One user-facing note per non-blank, non-# line; # lines are section comments. + +# --- For people writing Lua apps ------------------------------------------------------------ +Apps can clear their page. There was no way to remove the widgets an app had already created, so anything with a second screen drew it on top of the first. wada.ui.clear() takes the page back to empty, and any widget you were still holding from before the clear becomes inert rather than dangerous. +Apps can reach all your contacts. The list stopped at 100, and it only ever looked at the first 200 in the first place, so on a device with a lot of contacts most of them were unreachable and nothing said so. wada.mesh.contacts() now takes an offset and a limit, and wada.mesh.contact_count() tells an app how many there are to page through. +Apps can be three times larger. The limit was 64 KB, which an ordinary program can reach. It is 192 KB now. + +# --- T-Deck keyboard --------------------------------------------------------------------------- +Modifier latching could fail to turn on even with the right keyboard firmware installed. The firmware asked the keyboard what it was capable of exactly once, on the first read after boot, and the first read after boot is the one most likely to come back garbled because the keyboard controller is still starting. One bad answer meant no latching for the rest of the session, and rebooting simply asked again at the same unlucky moment. It now keeps asking for a second and a half before giving up, and says what it actually saw when it does. + +# --- For testers --------------------------------------------------------------------------------- +This is a small drop on top of beta_70, which carried the fix for the crash when leaving the known-regions screen. If you have not tried that yet, it is still the thing most worth confirming: add and remove a few regions, leave the screen, check the device stays up. +If your screen is not going to sleep, we are still investigating (issue 333) and it is not fixed here. diff --git a/scripts/audio_test.lua b/scripts/audio_test.lua new file mode 100644 index 0000000..c539d3a --- /dev/null +++ b/scripts/audio_test.lua @@ -0,0 +1,104 @@ +-- Manual WAV/MP3 transport test for wada.audio. +-- SD test file: /Music/test.mp3 +-- App-local file: /apps/audio_test.d/test.mp3 on the active app storage. +local ui, sys, timer = wada.ui, wada.sys, wada.timer +local audio = wada.audio +local C = ui.colors + +local SD_PATH = "sd:/Music/test.mp3" +local APP_PATH = "test.mp3" + +local app = {} +local source = APP_PATH +local source_line, state_line, detail_line, command_line + +local function show_command(ok, err) + command_line:set(ok and "Command: accepted" or ("Command: " .. tostring(err or "rejected"))) + command_line:color(ok and C.good or C.bad) +end + +local function refresh() + if not audio then return end + local status = audio.status() + state_line:set("State: " .. tostring(status.state)) + state_line:color(status.state == "error" and C.bad or + status.state == "playing" and C.good or C.text) + local detail = "Source: " .. tostring(status.source or "-") .. + " Format: " .. tostring(status.format or "-") + if status.error then detail = detail .. " Error: " .. tostring(status.error) end + detail_line:set(detail) + detail_line:color(status.error and C.bad or C.sub) +end + +function app.on_open(w, h) + local caps = sys.caps() + source = APP_PATH + if caps.audio_sd and wada.sd and wada.sd.list then + local entries = wada.sd.list("/") + if entries then source = SD_PATH end + end + + ui.label("Audio playback test", 6, 4, 14, C.accent) + ui.label("audio=" .. tostring(caps.audio) .. + " wav=" .. tostring(caps.audio_wav) .. + " mp3=" .. tostring(caps.audio_mp3), 6, 24, 12, C.sub) + source_line = ui.label("File: " .. source, 6, 44, 12, C.text) + source_line:width(w - 12) + state_line = ui.label("State: stopped", 6, 64, 12, C.text) + detail_line = ui.label("Source: - Format: -", 6, 82, 12, C.sub) + detail_line:width(w - 12) + command_line = ui.label("Command: ready", 6, 100, 12, C.sub) + command_line:width(w - 12) + + if not caps.audio or not audio then + command_line:set("Command: wada.audio unavailable") + command_line:color(C.bad) + return + end + + local gap = 5 + local button_w = math.max(54, (w - 12 - gap * 3) // 4) + local x = 6 + ui.button("Play", x, 116, button_w, 30, function() + local ok, err = audio.play(source) + show_command(ok, err) + refresh() + end) + x = x + button_w + gap + ui.button("Pause", x, 116, button_w, 30, function() + show_command(audio.pause()) + refresh() + end) + x = x + button_w + gap + ui.button("Resume", x, 116, button_w, 30, function() + show_command(audio.resume()) + refresh() + end) + x = x + button_w + gap + ui.button("Stop", x, 116, button_w, 30, function() + show_command(audio.stop()) + refresh() + end) + + if caps.audio_sd then + ui.button("Switch SD / app storage", 6, 150, math.min(w - 12, 190), 26, function() + source = source == SD_PATH and APP_PATH or SD_PATH + source_line:set("File: " .. source) + command_line:set("Command: source changed") + command_line:color(C.sub) + end) + end + + timer.every(200) + refresh() +end + +function app.on_tick() + refresh() +end + +function app.on_close() + if audio then audio.stop() end +end + +return app diff --git a/scripts/build/i18n-audit.py b/scripts/build/i18n-audit.py index 1466401..1f56efb 100644 --- a/scripts/build/i18n-audit.py +++ b/scripts/build/i18n-audit.py @@ -31,7 +31,7 @@ def source_keys(): keys = set() for f in glob.glob(os.path.join(ROOT, 'src/ui-touch/*.cpp')) + \ glob.glob(os.path.join(ROOT, 'src/ui-touch/*.h')): - src = open(f, encoding='utf-8').read() + src = strip_comments(open(f, encoding='utf-8').read()) # An optional LV_SYMBOL_* macro may precede the literal: TR(LV_SYMBOL_OK " Text"). # Requiring the group to START with a quote silently skipped every such # call - the audit's worst blind spot (the sheet rows never got checked). @@ -55,7 +55,7 @@ def source_keys(): # collecting keeps a translation alive, under-collecting deletes one. for f in glob.glob(os.path.join(ROOT, 'src/ui-touch/*.cpp')) + \ glob.glob(os.path.join(ROOT, 'src/ui-touch/*.h')): - src = open(f, encoding='utf-8').read() + src = strip_comments(open(f, encoding='utf-8').read()) for name in set(re.findall(r'TR\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\[', src)): for m in re.finditer(r'\b' + re.escape(name) + r'\s*\[[^\]]*\]\s*=\s*\{', src): i, depth = m.end(), 1 @@ -80,24 +80,22 @@ def source_keys(): # parameters and note which parameter, then pull the literal out of that # argument position at every call site. for f in _sources(): - src = open(f, encoding='utf-8').read() + src = strip_comments(open(f, encoding='utf-8').read()) for name, idx in _tr_wrapping_helpers(src): for arg in _call_args_at(src, name, idx): - parts = re.findall(r'"((?:[^"\\]|\\.)*)"', arg) - if not parts: - continue - k = unescape_c(''.join(parts)) - while k and 0xE000 <= ord(k[0]) <= 0xF8FF: - k = k[1:] - k = k.lstrip(' ') - if k.strip(): - keys.add(k) + for lit in literal_groups(arg): + k = unescape_c(lit) + while k and 0xE000 <= ord(k[0]) <= 0xF8FF: + k = k[1:] + k = k.lstrip(' ') + if k.strip(): + keys.add(k) # RANGE-FOR over a local table: `struct {...} rows[] = {{"Show contacts", ...}};` # then `for (auto& r : rows) TR(r.label)`. Same blind spot as the helper case -- # correct at runtime, invisible to a TR("literal") scan, so untranslatable. for f in _sources(): - src = open(f, encoding='utf-8').read() + src = strip_comments(open(f, encoding='utf-8').read()) for m in re.finditer(r'for\s*\(\s*(?:const\s+)?auto\s*&?\s*(\w+)\s*:\s*(\w+)\s*\)', src): var, tbl = m.group(1), m.group(2) brace = src.find('{', m.end()) @@ -119,7 +117,7 @@ def source_keys(): # TR(someFunc(...)): a helper that RETURNS one of several literals, translated # by the caller. Every literal it can return is a key. for f in _sources(): - src = open(f, encoding='utf-8').read() + src = strip_comments(open(f, encoding='utf-8').read()) for name in set(re.findall(r'TR\(\s*([A-Za-z_]\w*)\s*\(', src)): for d in re.finditer(r'\b' + re.escape(name) + r'\s*\([^;{}]*\)\s*\{', src): seg = src[d.end() - 1:_balanced(src, d.end() - 1)] @@ -130,7 +128,7 @@ def source_keys(): # LUA APPS: wada.sys.tr("...") (aliased to a local `tr` by convention). The # apps ship from the same catalog and their strings belong in the same files. for f in sorted(glob.glob(os.path.join(ROOT, 'deploy/apps/*/*/*.lua'))): - src = open(f, encoding='utf-8').read() + src = strip_comments(open(f, encoding='utf-8').read()) for lit in re.findall(r'\b(?:sys\.)?tr\(\s*"((?:[^"\\]|\\.)*)"', src): k = unescape_c(lit) if k.strip(): @@ -138,6 +136,66 @@ def source_keys(): return keys +def strip_comments(src): + """Blank out //... and /*...*/ while preserving string/char literals and line + count. The extractor scans raw text, so a quoted phrase inside a comment was + being collected as a translatable key -- that is how the Dutch string + "Geblokkeerde gebruikers", which only appears in a comment explaining how a + long translation degrades, ended up as a KEY in all thirteen files.""" + out, i, n = [], 0, len(src) + while i < n: + c = src[i] + if c == '"' or c == "'": + q = c; out.append(c); i += 1 + while i < n: + out.append(src[i]) + if src[i] == '\\' and i + 1 < n: + out.append(src[i + 1]); i += 2; continue + if src[i] == q: i += 1; break + i += 1 + continue + if c == '/' and i + 1 < n and src[i + 1] == '/': + while i < n and src[i] != '\n': i += 1 + continue + if c == '/' and i + 1 < n and src[i + 1] == '*': + i += 2 + while i + 1 < n and not (src[i] == '*' and src[i + 1] == '/'): + if src[i] == '\n': out.append('\n') + i += 1 + i += 2 + continue + out.append(c); i += 1 + return ''.join(out) + + +def literal_groups(arg): + """Keys from one call argument. + + A C compiler concatenates ADJACENT string literals, so `"a" "b"` is one + string. `cond ? "a" : "b"` is two, and joining them (which this used to do) + invented keys that exist nowhere: "Paste (move)Paste (copy)", + "Unblock Block", "Unfav Favorite". Those shipped to every translator in + v18. Only whitespace and an LV_SYMBOL_* macro may sit between literals of + one group; anything else starts a new one.""" + groups, cur, i, n = [], [], 0, len(arg) + while i < n: + c = arg[i] + if c == '"': + j, buf = i + 1, [] + while j < n: + if arg[j] == '\\' and j + 1 < n: buf.append(arg[j:j + 2]); j += 2; continue + if arg[j] == '"': break + buf.append(arg[j]); j += 1 + cur.append(''.join(buf)); i = j + 1; continue + if c.isspace(): i += 1; continue + m = re.match(r'LV_SYMBOL_[A-Z0-9_]+', arg[i:]) + if m: i += m.end(); continue + if cur: groups.append(cur); cur = [] + i += 1 + if cur: groups.append(cur) + return [''.join(g) for g in groups] + + def _sources(): return sorted(glob.glob(os.path.join(ROOT, 'src/ui-touch/*.cpp')) + glob.glob(os.path.join(ROOT, 'src/ui-touch/*.h'))) diff --git a/scripts/build/patch_meshcore_rv3028_define.py b/scripts/build/patch_meshcore_rv3028_define.py new file mode 100644 index 0000000..ab88690 --- /dev/null +++ b/scripts/build/patch_meshcore_rv3028_define.py @@ -0,0 +1,34 @@ +Import("env") +import os + +MARKER = "wadamesh-rv3028-guard-patch" +OLD = "#define RV3028_ADDRESS 0x52" +NEW = """#ifndef RV3028_ADDRESS +#define RV3028_ADDRESS 0x52 +#endif // wadamesh-rv3028-guard-patch""" + +path = os.path.join( + env.subst("$PROJECT_LIBDEPS_DIR"), + env.subst("$PIOENV"), + "MeshCore", + "src", + "helpers", + "AutoDiscoverRTCClock.cpp", +) + +if not os.path.isfile(path): + print("[patch_meshcore_rv3028_define] WARNING: %s not found (libdeps not fetched yet?)" % path) + print("[patch_meshcore_rv3028_define] WARNING: MeshCore NOT patched - re-run build once libdeps exist") +else: + with open(path) as f: + src = f.read() + + if MARKER in src: + print("[patch_meshcore_rv3028_define] already patched") + elif OLD in src: + with open(path, "w") as f: + f.write(src.replace(OLD, NEW, 1)) + print("[patch_meshcore_rv3028_define] patched RV3028_ADDRESS guard in AutoDiscoverRTCClock.cpp") + else: + print("[patch_meshcore_rv3028_define] WARNING: pattern not found - MeshCore version drift?") + print("[patch_meshcore_rv3028_define] WARNING: check AutoDiscoverRTCClock.cpp by hand, NOT patched") \ No newline at end of file diff --git a/scripts/lua-harness/README.md b/scripts/lua-harness/README.md index 08cfa7b..771b411 100644 --- a/scripts/lua-harness/README.md +++ b/scripts/lua-harness/README.md @@ -5,7 +5,7 @@ it ever touches a device. Not part of the firmware build. ```sh scripts/lua-harness/run.sh # gpscompass, all scenarios -scripts/lua-harness/run.sh deploy/apps/wardrive/1.0/wardrive.lua +scripts/lua-harness/run.sh deploy/apps/wardrive/1.1/wardrive.lua SCENARIO=declination scripts/lua-harness/run.sh # one scenario ``` diff --git a/scripts/lua-harness/harness.lua b/scripts/lua-harness/harness.lua index dc7adc2..3f8a205 100644 --- a/scripts/lua-harness/harness.lua +++ b/scripts/lua-harness/harness.lua @@ -10,6 +10,27 @@ local function checkint(v, what) assert(math.tointeger(v) ~= nil, what .. ": not local function checkcol(v, what) if v ~= nil then checkint(v, what .. " color") end end local function checkstr(v, what) assert(type(v) == "string" or type(v) == "number", what .. ": not a string") end +local function valid_utf8(text) + local i, n = 1, #text + while i <= n do + local first = text:byte(i) + local width = first <= 0x7F and 1 + or (first >= 0xC2 and first <= 0xDF and 2) + or (first >= 0xE0 and first <= 0xEF and 3) + or (first >= 0xF0 and first <= 0xF4 and 4) or 0 + if width == 0 or i + width - 1 > n then return false end + for j = 2, width do + local byte = text:byte(i + j - 1) + if byte < 0x80 or byte > 0xBF then return false end + end + local second = width > 1 and text:byte(i + 1) or 0 + if (first == 0xE0 and second < 0xA0) or (first == 0xED and second > 0x9F) or + (first == 0xF0 and second < 0x90) or (first == 0xF4 and second > 0x8F) then return false end + i = i + width + end + return true +end + -- instruction budget like guardedCall(): error() out of the hook, pcall catches local function guarded(budget, fn, ...) local count = 0 @@ -26,6 +47,13 @@ local cfg = {} -- per-scenario device config local widgets = { canvases = 0, labels = 0, buttons = 0, scroll = false, timer_ms = nil } local drawlog = { text = {}, circles = {}, ops = 0 } local storekv = {} +local audio_state = { state = "stopped", path = "", source = "", format = "", error = nil } +local audio_log = {} + +local function audio_host_close() + audio_state.state = "stopped" + audio_log[#audio_log + 1] = "release" +end local function mkcanvas(w, h) checkint(w, "canvas w"); checkint(h, "canvas h") @@ -77,7 +105,7 @@ local function mkbutton(text, x, y, w, h, fn) end local function build_wada() - local wada = { ui = {}, sys = {}, mesh = {}, store = {}, timer = {}, net = {} } + local wada = { ui = {}, sys = {}, mesh = {}, store = {}, timer = {}, net = {}, fs = {} } wada.ui.colors = { accent = 0x15B6A6, text = 0xE6E9ED, sub = 0x7A7F87, bg = 0x000000, panel = 0x15181B, bad = 0xD7574E, good = 0x53C06B } wada.ui.canvas = mkcanvas wada.ui.label = mklabel @@ -102,7 +130,12 @@ local function build_wada() wada.sys.beep = function() return false end wada.sys.caps = function() return { sdk_ext = cfg.caps.sdk_ext, keyboard = cfg.caps.keyboard, touch = cfg.caps.touch, sd = true, compass = cfg.caps.compass, - accel = cfg.caps.accel } end + accel = cfg.caps.accel, discover = cfg.caps.discover, + sd_list = cfg.caps.sd_list or false, + audio = cfg.caps.audio or false, + audio_wav = cfg.caps.audio or false, + audio_mp3 = cfg.caps.audio or false, + audio_sd = cfg.caps.audio_sd or false } end if cfg.caps.sdk_ext then wada.sys.battery = function() return { mv = 3900, pct = 70, charging = false } end wada.sys.gps = function() return cfg.gps and cfg.gps() or nil end @@ -118,6 +151,12 @@ local function build_wada() wada.mesh.self = function() return { name = "me", lat = cfg.self_lat or 0, lon = cfg.self_lon or 0 } end wada.mesh.stats = function() return {} end wada.mesh.rx_log = function() return {} end + wada.mesh.discover = function() return "probe-1" end + wada.mesh.discovered = function() return cfg.discovered or {} end + wada.mesh.discover_clear = function() cfg.discovered = {} end + + wada.fs.append = function(name, data) checkstr(name, "fs.append name"); checkstr(data, "fs.append data"); return true end + wada.fs.remove = function(name) checkstr(name, "fs.remove name"); return true end wada.store.get = function(k, d) assert(type(k) == "string"); local v = storekv[k]; if v == nil then return d end return v end wada.store.set = function(k, v) assert(type(k) == "string", "store key must be a string") @@ -126,6 +165,56 @@ local function build_wada() wada.timer.every = function(ms) checkint(ms, "timer.every"); if ms < 33 then ms = 33 end; widgets.timer_ms = ms end wada.timer.stop = function() widgets.timer_ms = nil end + + if cfg.caps.audio then + wada.audio = {} + wada.audio.play = function(path) + assert(type(path) == "string", "audio.play path must be a string") + local source = "app" + if path:sub(1, 3) == "sd:" then + if not cfg.caps.audio_sd then return nil, "no sd" end + local card_path = path:sub(4) + if card_path:sub(1, 1) ~= "/" or card_path:find("//", 1, true) or + card_path:find("/../", 1, true) or card_path:sub(-3) == "/.." or + card_path:sub(-2) == "/." or card_path:sub(-1) == "/" then + return nil, "bad path" + end + source = "sd" + elseif #path == 0 or #path > 32 or path:sub(1, 1) == "." or + not path:match("^[A-Za-z0-9._-]+$") then + return nil, "bad path" + end + local format = path:lower():sub(-4) + if format ~= ".wav" and format ~= ".mp3" then return nil, "unsupported format" end + audio_state = { state = "playing", path = path, source = source, + format = format:sub(2), error = nil } + audio_log[#audio_log + 1] = "play:" .. path + return true + end + wada.audio.pause = function() + if audio_state.state ~= "playing" then return false end + audio_state.state = "paused"; audio_log[#audio_log + 1] = "pause"; return true + end + wada.audio.resume = function() + if audio_state.state ~= "paused" then return false end + audio_state.state = "playing"; audio_log[#audio_log + 1] = "resume"; return true + end + wada.audio.stop = function() + if audio_state.state ~= "playing" and audio_state.state ~= "paused" then return false end + audio_state.state = "stopped"; audio_log[#audio_log + 1] = "stop"; return true + end + wada.audio.status = function() + local out = {} + for key, value in pairs(audio_state) do out[key] = value end + return out + end + end + if cfg.caps.sd_list then + wada.sd = { list = function(path) + assert(path == "/", "mock SD only exposes the root") + return {} + end } + end return wada end @@ -159,6 +248,8 @@ local function reset_world() widgets = { canvases = 0, labels = 0, buttons = 0, scroll = false, timer_ms = nil } labels, buttons, toasts, drawlog = {}, {}, {}, { text = {}, circles = {}, ops = 0 } clock_ms = 1000 + audio_state = { state = "stopped", path = "", source = "", format = "", error = nil } + audio_log = {} end -- simulated magnetometer: Earth field 0.45 G at true heading `deg` (device frame == @@ -204,6 +295,36 @@ local contacts_fixture = { local scenarios = {} +scenarios.wardrive_utf8 = function() + cfg = { + w = 320, h = 196, + caps = { sdk_ext = true, keyboard = true, touch = false, compass = false, accel = false, discover = true }, + discovered = { + { pubkey = "01020304", name = "Ouderkerk☀️", type = 2, + rssi = -72, snr = 7.25, their_snr = 6.5, hops = 0 } + } + } + cfg.gps = function() + return { lat = 52.295, lon = 4.907, lat_e6 = 52295000, lon_e6 = 4907000, + sats = 9, alt_m = 3, time = 1787620000 } + end + storekv = {} + wada = build_wada() + local app = load_app() + assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h)) + assert(buttons[1] and buttons[1].fn, "Wardrive Start button missing") + assert(guarded(BUDGET, buttons[1].fn)) + tick(app, 1, 100) + tick(app, 1, 4000) + local found = false + for _, label in ipairs(labels) do + assert(valid_utf8(label.text), "Wardrive rendered invalid UTF-8") + if label.text:find("Ouderkerk☀", 1, true) then found = true end + end + assert(found, "emoji-bearing repeater name was not rendered") + if app.on_close then guarded(BUDGET, app.on_close) end +end + -- The declination model, as it actually ships. Rather than testing a copy in -- out/wmm, this pulls the do-block straight out of the app file that gets -- sideloaded, so an inlining mistake fails here instead of on the device. @@ -683,6 +804,61 @@ scenarios.tanmatsu = function() if app.on_close then guarded(BUDGET, app.on_close) end end +scenarios.audio_api = function() + cfg = { w = 320, h = 196, + caps = { sdk_ext = true, keyboard = true, touch = false, + audio = true, audio_sd = true, sd_list = true } } + wada = build_wada() + local caps = wada.sys.caps() + assert(caps.audio and caps.audio_wav and caps.audio_mp3 and caps.audio_sd, + "audio capability flags do not match the WAV/MP3 contract") + assert(wada.audio and wada.audio.play and wada.audio.pause and wada.audio.resume and + wada.audio.stop and wada.audio.status, "wada.audio API is incomplete") + + assert(wada.audio.play("track.wav")) + local status = wada.audio.status() + assert(status.state == "playing" and status.path == "track.wav" and + status.source == "app" and status.format == "wav", "app-storage play status is wrong") + assert(wada.audio.pause() and wada.audio.status().state == "paused", "pause failed") + assert(wada.audio.resume() and wada.audio.status().state == "playing", "resume failed") + assert(wada.audio.play("next.wav") and wada.audio.status().path == "next.wav", + "a second play must replace the active track") + assert(wada.audio.play("sd:/Music/card.wav")) + assert(wada.audio.status().source == "sd", "sd: source was not reported") + + local value, err = wada.audio.play("../escape.wav") + assert(value == nil and err == "bad path", "app sandbox traversal was accepted") + value, err = wada.audio.play("sd:/Music/../escape.wav") + assert(value == nil and err == "bad path", "SD traversal was accepted") + assert(wada.audio.play("track.mp3") and wada.audio.status().format == "mp3", + "MP3 playback was not accepted or reported") + value, err = wada.audio.play("track.flac") + assert(value == nil and err == "unsupported format", "unknown audio format was accepted") + + assert(wada.audio.stop() and wada.audio.status().state == "stopped", "stop failed") + assert(wada.audio.play("close.wav")) + audio_host_close() + assert(wada.audio.status().state == "stopped" and audio_log[#audio_log] == "release", + "host close did not release playback") + print(" transport + storage sandbox: PASS (" .. #audio_log .. " host calls)") + + if APP_PATH:match("audio_test%.lua$") then + local app = load_app() + assert(guarded(BUDGET, app.on_open, cfg.w, cfg.h)) + assert(#buttons == 5, "audio test app did not create all transport/source buttons") + buttons[1].fn(); assert(wada.audio.status().state == "playing", "Play button failed") + buttons[2].fn(); assert(wada.audio.status().state == "paused", "Pause button failed") + buttons[3].fn(); assert(wada.audio.status().state == "playing", "Resume button failed") + buttons[4].fn(); assert(wada.audio.status().state == "stopped", "Stop button failed") + buttons[5].fn(); buttons[1].fn() + assert(wada.audio.status().source == "app", "source switch did not select app storage") + guarded(BUDGET, app.on_tick) + guarded(BUDGET, app.on_close) + assert(wada.audio.status().state == "stopped", "test app close did not stop playback") + print(" scripts/audio_test.lua UI: PASS") + end +end + -- instruction cost of one heavy tick (redraw forced every tick by spinning the heading) scenarios.cost = function() cfg = { w = 320, h = 196, caps = { sdk_ext = true, keyboard = true, touch = false, compass = true, accel = true }, @@ -711,7 +887,9 @@ scenarios.cost = function() assert(worst < BUDGET / 4, "tick too expensive") end -local order = { "declination", "align_nofix", "bearings_absolute", "m9", "r8", "v4", "pager", "pager_portrait_jumbo", "tanmatsu", "cost" } +local order = APP_PATH:find("/wardrive/", 1, true) + and { "wardrive_utf8" } + or { "declination", "align_nofix", "bearings_absolute", "m9", "r8", "v4", "pager", "pager_portrait_jumbo", "tanmatsu", "audio_api", "cost" } for _, name in ipairs(order) do if SCENARIO == "all" or SCENARIO == name then print("== " .. name) diff --git a/scripts/sideload_app.py b/scripts/sideload_app.py index c74525e..3b29497 100644 --- a/scripts/sideload_app.py +++ b/scripts/sideload_app.py @@ -22,6 +22,7 @@ Usage: scripts/sideload_app.py --port /dev/cu.wchusbserial10 deploy/apps/gpscompass/1.0 scripts/sideload_app.py --port ... --reboot deploy/apps/gpscompass/1.0/gpscompass.lua scripts/sideload_app.py --port ... --dest /lang deploy/apps/lang/11/de.lang + scripts/sideload_app.py --port ... --remote /apps/audio_test.d/test.mp3 ~/Music/test.mp3 Pass an app VERSION directory to send its .lua + .json, or individual files. --reboot restarts the device afterwards so the drawer/Store rescan @@ -152,6 +153,7 @@ def main(): ap.add_argument("--port", required=True) ap.add_argument("--baud", type=int, default=115200) ap.add_argument("--dest", default="/apps", help="/apps (default) or /lang") + ap.add_argument("--remote", help="exact remote path for one file, including /apps/.d/") ap.add_argument("--reboot", action="store_true", help="reboot the device afterwards") args = ap.parse_args() @@ -170,10 +172,19 @@ def main(): else: sys.exit("not found: " + p) + if args.remote: + if len(files) != 1: + sys.exit("--remote requires exactly one input file") + if not args.remote.startswith(("/apps/", "/lang/")): + sys.exit("--remote must begin with /apps/ or /lang/") + s = open_port(args.port, args.baud) try: - for f in files: - push(s, f, "%s/%s" % (args.dest.rstrip("/"), os.path.basename(f))) + if args.remote: + push(s, files[0], args.remote) + else: + for f in files: + push(s, f, "%s/%s" % (args.dest.rstrip("/"), os.path.basename(f))) if args.reboot: s.write(b"reboot\n") s.flush() diff --git a/src/DataStore.cpp b/src/DataStore.cpp index 4919f10..cd698b8 100644 --- a/src/DataStore.cpp +++ b/src/DataStore.cpp @@ -230,6 +230,13 @@ bool DataStore::mkdirRooted(FILESYSTEM* fs, const char* dir) { return fs->exists(p) || fs->mkdir(p); } +File DataStore::openWriteRootedFlatSafe(FILESYSTEM* fs, const char* filename) { + // Do not pass create=true here. Arduino's VFS implementation interprets it + // as "mkdir every parent first", but SPIFFS is flat and returns ENOTSUP for + // mkdir while still accepting slash-containing file keys. + return fs->open(_rp(filename), "w"); +} + bool DataStore::removeRooted(FILESYSTEM* fs, const char* filename) { return fs->remove(_rp(filename)); } diff --git a/src/DataStore.h b/src/DataStore.h index 0a547c3..539f8e9 100644 --- a/src/DataStore.h +++ b/src/DataStore.h @@ -109,6 +109,7 @@ public: File openAppend(FILESYSTEM* fs, const char* filename); // create if missing bool fileExists(FILESYSTEM* fs, const char* filename); bool mkdirRooted(FILESYSTEM* fs, const char* dir); // true if it exists afterwards + File openWriteRootedFlatSafe(FILESYSTEM* fs, const char* filename); // SPIFFS accepts slash keys but no mkdir bool removeRooted(FILESYSTEM* fs, const char* filename); bool renameFile(FILESYSTEM* fs, const char* from, const char* to); #endif diff --git a/src/MyMesh.cpp b/src/MyMesh.cpp index 8f482cc..b769e62 100644 --- a/src/MyMesh.cpp +++ b/src/MyMesh.cpp @@ -5445,7 +5445,7 @@ void MyMesh::checkCLIRescueCmd() { // opens the file, "fadd " lines append to it // (self-checking, see cliPutChunk), "fend" closes it. // Same physical-access trust level as "rm" and "erase" above, with a - // NARROWER scope: only the two sideload directories the Store writes. + // NARROWER scope: Store files plus one app's private .d directory. cliPutBegin(&cli_command[5]); } else if (memcmp(cli_command, "fadd ", 5) == 0) { cliPutChunk(&cli_command[5]); @@ -5470,24 +5470,51 @@ void MyMesh::checkCLIRescueCmd() { // "Error: ...". void MyMesh::cliPutBegin(const char* path) { if (_cli_put) { _cli_put.close(); _cli_put_len = 0; } + _cli_put_ended = false; const char* dir = nullptr; if (memcmp(path, "/apps/", 6) == 0) dir = "/apps"; else if (memcmp(path, "/lang/", 6) == 0) dir = "/lang"; if (!dir) { Serial.println("Error: path must be /apps/ or /lang/"); return; } const char* name = path + 6; - const size_t nlen = strlen(name); - if (nlen == 0 || nlen > 40) { Serial.println("Error: bad file name length"); return; } - for (size_t i = 0; i < nlen; i++) { - const char c = name[i]; - const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || - c == '.' || c == '_' || c == '-'; + const char* slash = strchr(name, '/'); + const char* leaf = slash ? slash + 1 : name; + const size_t dir_len = slash ? (size_t)(slash - name) : 0; + const size_t leaf_len = strlen(leaf); + if (!leaf_len || leaf_len > 40 || (slash && leaf_len > 32) || + leaf[0] == '.' || strchr(leaf, '/')) { + Serial.println("Error: bad file name"); + return; + } + for (size_t i = 0; i < leaf_len; i++) { + const char c = leaf[i]; + const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; if (!ok) { Serial.println("Error: file name may only use A-Z a-z 0-9 . _ -"); return; } } - if (name[0] == '.') { Serial.println("Error: file name may not start with '.'"); return; } + + char app_data_dir[48] = ""; + if (slash) { + // The only nested destination is /apps/.d/, matching wada.fs. + if (strcmp(dir, "/apps") != 0 || dir_len < 3 || dir_len > 25 || + name[dir_len - 2] != '.' || name[dir_len - 1] != 'd' || name[0] == '.') { + Serial.println("Error: nested path must be /apps/.d/"); + return; + } + for (size_t i = 0; i + 2 < dir_len; i++) { + const char c = name[i]; + const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' || c == '-'; + if (!ok) { Serial.println("Error: bad app id"); return; } + } + snprintf(app_data_dir, sizeof app_data_dir, "/apps/%.*s", (int)dir_len, name); + } FILESYSTEM* fs = _store->getHotDataFS(); if (!fs) { Serial.println("Error: storage not ready"); return; } - if (!_store->mkdirRooted(fs, dir)) { Serial.printf("Error: cannot create %s\n", dir); return; } - _cli_put = _store->openWrite(fs, path); + // Hierarchical filesystems create these directories. SPIFFS returns ENOTSUP + // but accepts the complete slash-containing path as a flat file key. + _store->mkdirRooted(fs, dir); + if (app_data_dir[0]) _store->mkdirRooted(fs, app_data_dir); + _cli_put = _store->openWriteRootedFlatSafe(fs, path); if (!_cli_put) { Serial.printf("Error: cannot open %s for writing\n", path); return; } _cli_put_len = 0; Serial.printf("ok fput %s\n", path); @@ -5518,7 +5545,7 @@ void MyMesh::cliPutChunk(const char* args) { if ((s & 0xFFFF) != (sum & 0xFFFF)) { Serial.printf("Error: checksum at %u\n", (unsigned)_cli_put_len); return; } if (olen && _cli_put.write(buf, olen) != olen) { Serial.println("Error: write failed (card full?)"); - _cli_put.close(); _cli_put_len = 0; + _cli_put.close(); _cli_put_len = 0; _cli_put_ended = false; return; } _cli_put_len += olen; @@ -5526,9 +5553,15 @@ void MyMesh::cliPutChunk(const char* args) { } void MyMesh::cliPutEnd() { - if (!_cli_put) { Serial.println("Error: no file open (fput first)"); return; } + if (!_cli_put) { + if (_cli_put_ended) Serial.printf("ok fend %u bytes\n", (unsigned)_cli_put_last_len); + else Serial.println("Error: no file open (fput first)"); + return; + } _cli_put.close(); - Serial.printf("ok fend %u bytes\n", (unsigned)_cli_put_len); + _cli_put_last_len = _cli_put_len; + _cli_put_ended = true; + Serial.printf("ok fend %u bytes\n", (unsigned)_cli_put_last_len); _cli_put_len = 0; } #endif // ESP32 diff --git a/src/MyMesh.h b/src/MyMesh.h index 4fb8a51..8a8344a 100644 --- a/src/MyMesh.h +++ b/src/MyMesh.h @@ -415,6 +415,17 @@ public: return sendTelemetryRequestForUI(recipient); } + /** Interactive logins must rediscover the route. MeshCore 1.16 repeaters + * answer a direct login by flooding the response without refreshing their + * return path; a flooded login returns PATH + LOGIN_OK and refreshes both + * sides before the deferred request or first admin command is sent. */ + void uiResetPathForLogin(ContactInfo& recipient) { + if (recipient.out_path_len == OUT_PATH_UNKNOWN) return; + uiResetContactPath(recipient.id.pub_key); + recipient.out_path_len = OUT_PATH_UNKNOWN; + memset(recipient.out_path, 0, sizeof(recipient.out_path)); + } + /** Touch-UI manual STATUS/TELEMETRY request that DEFERS the REQ until the * guest LOGIN is acknowledged. The chained helpers above fire LOGIN and REQ * back-to-back, but a repeater drops a PAYLOAD_TYPE_REQ from a sender it @@ -435,6 +446,7 @@ public: return (kind == UiReqKind::Telemetry) ? sendTelemetryRequestForUI(recipient) : sendStatusPingForUI(recipient); } + uiResetPathForLogin(recipient); uint32_t login_est = 0; int r = sendLogin(recipient, "", login_est); if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { @@ -453,6 +465,7 @@ public: * AbstractUITask::onAdminLoginResult so the UI can flip from "logging * in…" to "logged in" (or "failed"). */ int uiSendAdminLogin(ContactInfo& recipient, const char* password) { + uiResetPathForLogin(recipient); uint32_t est = 0; int r = sendLogin(recipient, password ? password : "", est); if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { @@ -1296,6 +1309,8 @@ private: // Serial sideload state ("fput" / "fadd" / "fend"): the file being written. File _cli_put; uint32_t _cli_put_len = 0; + uint32_t _cli_put_last_len = 0; + bool _cli_put_ended = false; void cliPutBegin(const char* path); void cliPutChunk(const char* args); void cliPutEnd(); diff --git a/src/helpers/esp32/TouchPrefsStore.cpp b/src/helpers/esp32/TouchPrefsStore.cpp index 65a875a..07aef96 100644 --- a/src/helpers/esp32/TouchPrefsStore.cpp +++ b/src/helpers/esp32/TouchPrefsStore.cpp @@ -1849,6 +1849,10 @@ bool touchPrefsGetEdgeScroll() { if (!s_begun) touchPrefsBegin(); return s_ void touchPrefsSetEdgeScroll(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("tb_edgesc", on ? 1 : 0); } bool touchPrefsGetLockOnScreenOff() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("lock_off", 0) != 0; } void touchPrefsSetLockOnScreenOff(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("lock_off", on ? 1 : 0); } +bool touchPrefsGetGlanceWhenLocked() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("glance_lck", 0) != 0; } +void touchPrefsSetGlanceWhenLocked(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("glance_lck", on ? 1 : 0); } +bool touchPrefsGetGlanceEnabled() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("glance_en", 1) != 0; } +void touchPrefsSetGlanceEnabled(bool on) { if (!s_begun) touchPrefsBegin(); prefsPutUChar("glance_en", on ? 1 : 0); } #if defined(HAS_TANMATSU) // only the Tanmatsu has the message LED — keep S3 (T-Deck/V4) bins unchanged bool touchPrefsGetMsgLed() { if (!s_begun) touchPrefsBegin(); return s_prefs.getUChar("msg_led", 1) != 0; } // default ON diff --git a/src/helpers/esp32/TouchPrefsStore.h b/src/helpers/esp32/TouchPrefsStore.h index b528898..f3b0151 100644 --- a/src/helpers/esp32/TouchPrefsStore.h +++ b/src/helpers/esp32/TouchPrefsStore.h @@ -451,6 +451,10 @@ bool touchPrefsGetEdgeScroll(); // push cursor past edge to scrol void touchPrefsSetEdgeScroll(bool on); bool touchPrefsGetLockOnScreenOff(); // idle screen-off auto-locks; only a deliberate hold wakes (default false) void touchPrefsSetLockOnScreenOff(bool on); +bool touchPrefsGetGlanceWhenLocked(); // "at a glance" also fires while manually/idle locked, not just unlocked+dimmed (default false) +void touchPrefsSetGlanceWhenLocked(bool on); +bool touchPrefsGetGlanceEnabled(); // master "at a glance" feature toggle (default true) +void touchPrefsSetGlanceEnabled(bool on); /** Per-channel mute, keyed by channel name. Bit 0 = mute messages, bit 1 = * mute @-mentions. Suppresses the notification SOUND for that channel (the diff --git a/src/helpers/input/LatchedModifier.h b/src/helpers/input/LatchedModifier.h new file mode 100644 index 0000000..4a30c8a --- /dev/null +++ b/src/helpers/input/LatchedModifier.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +class LatchedModifier { + public: + static constexpr uint32_t DOUBLE_TAP_MS = 400; + + void press() { + held_ = true; + used_while_held_ = false; + } + + void release(uint32_t now_ms) { + held_ = false; + if (discard_release_) { + discard_release_ = false; + used_while_held_ = false; + return; + } + if (used_while_held_) return; + tap(now_ms); + } + + void tap(uint32_t now_ms) { + if (locked_) { + clearLatched(); + return; + } + if (one_shot_ && (uint32_t)(now_ms - last_tap_ms_) <= DOUBLE_TAP_MS) { + one_shot_ = false; + locked_ = true; + last_tap_ms_ = 0; + return; + } + one_shot_ = true; + last_tap_ms_ = now_ms; + } + + bool consumeForKey() { + const bool active_now = held_ || one_shot_ || locked_; + if (held_) used_while_held_ = true; + if (one_shot_) { + one_shot_ = false; + last_tap_ms_ = 0; + } + return active_now; + } + + void markHeldUsed() { + if (!held_) return; + used_while_held_ = true; + one_shot_ = false; + last_tap_ms_ = 0; + } + + void clearLatched() { + one_shot_ = false; + locked_ = false; + last_tap_ms_ = 0; + } + + void discard() { + clearLatched(); + if (held_) { + used_while_held_ = true; + discard_release_ = true; + } + } + + void baseline(bool physically_held) { + clearLatched(); + held_ = physically_held; + used_while_held_ = physically_held; + discard_release_ = physically_held; + } + + bool held() const { return held_; } + bool latched() const { return one_shot_ || locked_; } + bool active() const { return held_ || one_shot_ || locked_; } + bool locked() const { return locked_; } + + private: + bool held_ = false; + bool used_while_held_ = false; + bool one_shot_ = false; + bool locked_ = false; + bool discard_release_ = false; + uint32_t last_tap_ms_ = 0; +}; \ No newline at end of file diff --git a/src/helpers/input/PagerKeyboard.cpp b/src/helpers/input/PagerKeyboard.cpp index e357368..4e39a1d 100644 --- a/src/helpers/input/PagerKeyboard.cpp +++ b/src/helpers/input/PagerKeyboard.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include "PagerKeyboardState.h" #ifndef KB_INT #define KB_INT 6 @@ -14,30 +14,10 @@ #ifndef KB_BACKLIGHT #define KB_BACKLIGHT 46 #endif -#define KB_ROWS 4 -#define KB_COLS 10 - -// Matrix legend, s_keymap[row][col] — same physical keyboard PCB as -// trail-mate's working LR1121 pager build, cross-checked there. '\0' = no -// character at that position (dead cell, or intercepted as a modifier below). -static constexpr char s_keymap[KB_ROWS][KB_COLS] = { - {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'}, - {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\r'}, - {'\0', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '\0', '\0'}, - {' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'}, -}; -// Alt layer (symbols/numbers) — this hardware has no separate physical Symbol -// key, so Alt alone drives it (matches trail-mate's has_symbol_key=false path). -static constexpr char s_symbolMap[KB_ROWS][KB_COLS] = { - {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}, - {'*', '/', '+', '-', '=', ':', '\'', '"', '@', '\0'}, - {'\0', '_', '$', ';', '?', '!', ',', '.', '\0', '\0'}, - {' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'}, -}; - // Modifier/special-key positions: 0-based (row*KB_COLS + col), matching the -// TCA8418 raw event's (code & 0x7F) - 1. Alt is a hold (symbol layer while -// held); Shift is a hold too (momentary uppercase on the base layer, real +// TCA8418 raw event's (code & 0x7F) - 1. Alt selects symbols while held, a +// solo tap selects them for one key, and a double tap locks the layer. Shift +// is a hold too (momentary uppercase on the base layer, real // Shift-key semantics) — held Alt THEN a Shift press instead chords into // Alt+Shift, reported via s_alt_shift_chord_pending. What that chord DOES is // a UI-level decision (UITask.cpp): Caps Lock toggle while editing a text @@ -55,29 +35,21 @@ static constexpr char s_symbolMap[KB_ROWS][KB_COLS] = { // harmless in practice since the intended gesture is // hold-Alt-tap-Shift-release-both, not holding all three simultaneously. // Backspace (row2,col9) sits one column over from Shift, so Alt+Backspace -// doesn't share this exact ghosting risk with any base-layer letter. -static constexpr uint8_t kAltPos = 2 * KB_COLS + 0; // row2,col0 ('\0' in both layers) -static constexpr uint8_t kShiftPos = 2 * KB_COLS + 8; // row2,col8 ('\0' in both layers) -static constexpr uint8_t kBackspacePos = 2 * KB_COLS + 9; // row2,col9 ('\0' in both layers) -static constexpr uint8_t kSpacePos = 3 * KB_COLS + 0; // row3,col0 (' ' in both layers) +// doesn't share this exact ghosting risk with any base-layer letter. The maps +// and positions live in PagerKeyboardState so host tests exercise the exact +// production translation logic. static Adafruit_TCA8418 s_kb; static bool s_inited = false; -static bool s_alt = false; -static bool s_alt_used = false; // Alt consumed as a modifier since it was last pressed -static bool s_alt_tap_pending = false; // Alt pressed+released with nothing else happening meanwhile -static bool s_caps = false; -static bool s_shift_held = false; // momentary Shift, mirrors s_backspace_held/s_space_held -static bool s_alt_shift_chord_pending = false; // one-shot, see pagerKeyboardConsumeAltShiftChord() -static bool s_alt_backspace_chord_pending = false; // one-shot, see pagerKeyboardConsumeAltBackspaceChord() -static bool s_backspace_held = false; -static bool s_space_held = false; +static PagerKeyboardState s_state; -// Single-producer (poll) / single-consumer (UI thread) ring — same pattern as -// TDeckKeyboard.cpp; byte indices are atomic enough for SPSC without a lock. -static volatile uint8_t s_ring[16]; -static volatile uint8_t s_head = 0; -static volatile uint8_t s_tail = 0; +// Single-producer (poll) / single-consumer ring. Polling currently happens on +// the UI task, but the short critical section preserves the header's contract +// if a future board moves I2C polling to another core. +static portMUX_TYPE s_ring_mux = portMUX_INITIALIZER_UNLOCKED; +static uint8_t s_ring[16]; +static uint8_t s_head = 0; +static uint8_t s_tail = 0; static bool s_bl_ready = false; // This framework's Arduino-ESP32 core only has the channel-based LEDC API @@ -88,16 +60,19 @@ static bool s_bl_ready = false; static constexpr uint8_t kKbBacklightPwmChannel = 0; static void ringPush(uint8_t c) { + portENTER_CRITICAL(&s_ring_mux); const uint8_t nh = (uint8_t)((s_head + 1) & 15); if (nh != s_tail) { // drop if the ring is full s_ring[s_head] = c; s_head = nh; } + portEXIT_CRITICAL(&s_ring_mux); } void pagerKeyboardBegin() { if (s_inited) return; - s_inited = s_kb.begin(TCA8418_DEFAULT_ADDR, &Wire) && s_kb.matrix(KB_ROWS, KB_COLS); + s_inited = s_kb.begin(TCA8418_DEFAULT_ADDR, &Wire) && + s_kb.matrix(PagerKeyboardState::ROWS, PagerKeyboardState::COLS); if (!s_inited) return; s_kb.flush(); pinMode(KB_INT, INPUT_PULLUP); // TCA8418 INT is open-drain active-low; not ISR-driven here (see .h) @@ -115,58 +90,20 @@ void pagerKeyboardPoll() { const bool pressed = (raw & 0x80) != 0; const uint8_t code = (uint8_t)((raw & 0x7F) - 1); - if (code == kAltPos) { - // Solo tap (press+release, nothing else in between) vs. a modifier hold - // (symbol-layer typing, or the rotary encoder's Alt+turn via - // pagerKeyboardMarkAltUsed()) — only the former queues a pending tap. - if (pressed) { s_alt = true; s_alt_used = false; } - else { if (!s_alt_used) s_alt_tap_pending = true; s_alt = false; } - continue; - } - // Any other key event while Alt is held means Alt is being used as a - // modifier, not tapped solo — cancels the pending-tap interpretation. - if (s_alt && pressed) s_alt_used = true; - if (code == kShiftPos) { - if (pressed) { - if (s_alt) s_alt_shift_chord_pending = true; // Alt (Fn) held + Shift press = chord (UI decides the effect) - else s_shift_held = true; - } else { - s_shift_held = false; - } - continue; - } - if (code == kBackspacePos) { - if (pressed) { - // Alt+Backspace is a distinct chord (jump Home, everywhere -- see - // pagerKeyboardConsumeAltBackspaceChord()), not a delete: suppress - // both the '\b' ring-push AND s_backspace_held, so the plain- - // Backspace hold gestures (back / unlock) never also see this press. - if (s_alt) s_alt_backspace_chord_pending = true; - else { s_backspace_held = true; ringPush('\b'); } - } else { - s_backspace_held = false; - } - continue; - } - if (code == kSpacePos) { s_space_held = pressed; if (pressed) ringPush(' '); continue; } - if (!pressed) continue; // base/symbol keys only emit on press - - const uint8_t row = code / KB_COLS; - const uint8_t col = code % KB_COLS; - if (row >= KB_ROWS) continue; // a GPIO event outside the matrix, not a key - - char c = s_alt ? s_symbolMap[row][col] : s_keymap[row][col]; - if (c == '\0') continue; - // Caps Lock and a held Shift both mean "uppercase," base layer only. - if ((s_caps || s_shift_held) && !s_alt) c = (char)toupper((unsigned char)c); - ringPush((uint8_t)c); + const uint8_t key = s_state.event(code, pressed, millis()); + if (key) ringPush(key); } } int pagerKeyboardReadKey() { - if (s_tail == s_head) return 0; + portENTER_CRITICAL(&s_ring_mux); + if (s_tail == s_head) { + portEXIT_CRITICAL(&s_ring_mux); + return 0; + } const uint8_t c = s_ring[s_tail]; s_tail = (uint8_t)((s_tail + 1) & 15); + portEXIT_CRITICAL(&s_ring_mux); return c; } @@ -179,32 +116,26 @@ void pagerKeyboardSetBacklight(uint8_t level) { ledcWrite(kKbBacklightPwmChannel, level); } -bool pagerKeyboardAltHeld() { return s_alt; } +bool pagerKeyboardAltHeld() { return s_state.altHeld(); } -void pagerKeyboardMarkAltUsed() { s_alt_used = true; } +void pagerKeyboardMarkAltUsed() { s_state.markAltUsed(); } -bool pagerKeyboardConsumeAltTap() { - if (!s_alt_tap_pending) return false; - s_alt_tap_pending = false; - return true; +void pagerKeyboardDiscardAlt() { + s_state.discardAlt(); } -bool pagerKeyboardBackspaceHeld() { return s_backspace_held; } +bool pagerKeyboardBackspaceHeld() { return s_state.backspaceHeld(); } -bool pagerKeyboardSpaceHeld() { return s_space_held; } +bool pagerKeyboardSpaceHeld() { return s_state.spaceHeld(); } bool pagerKeyboardConsumeAltShiftChord() { - if (!s_alt_shift_chord_pending) return false; - s_alt_shift_chord_pending = false; - return true; + return s_state.consumeAltShiftChord(); } -void pagerKeyboardToggleCaps() { s_caps = !s_caps; } +void pagerKeyboardToggleCaps() { s_state.toggleCaps(); } bool pagerKeyboardConsumeAltBackspaceChord() { - if (!s_alt_backspace_chord_pending) return false; - s_alt_backspace_chord_pending = false; - return true; + return s_state.consumeAltBackspaceChord(); } #endif diff --git a/src/helpers/input/PagerKeyboard.h b/src/helpers/input/PagerKeyboard.h index 774e552..7d3eca4 100644 --- a/src/helpers/input/PagerKeyboard.h +++ b/src/helpers/input/PagerKeyboard.h @@ -3,8 +3,8 @@ // T-LoRa Pager physical QWERTY keyboard: a TCA8418 I2C matrix controller (4 // rows x 10 cols, addr 0x34) on the shared I2C bus (SDA 3 / SCL 2). Unlike -// the T-Deck's keyboard (a second MCU that resolves ASCII itself before we -// ever see a byte), the TCA8418 only reports raw row/col matrix events — the +// the T-Deck's keyboard (a second MCU, with raw mode only on newer firmware), +// the TCA8418 always reports raw row/col matrix events — the // keymap + shift/sym/alt state machine lives HERE, so pagerKeyboardReadKey() // produces the same final ASCII/control-code stream handleHwKey() already // expects from the T-Deck; no UI-side changes needed to consume it. @@ -13,7 +13,7 @@ // must be called from a single, consistent context each tick (whichever task // ends up owning it — wired in a later milestone; this board has no // pre-existing shared-bus task the way the T-Deck's touch poll does). -// pagerKeyboardReadKey() only pops from a lock-free ring and is safe to call +// pagerKeyboardReadKey() only pops from a critical-section-protected ring and is safe to call // from the UI thread regardless of which context polls. #if defined(HAS_PAGER_KEYBOARD) && defined(ESP32) @@ -46,15 +46,12 @@ bool pagerKeyboardAltHeld(); /** Mark the currently-held Alt as "used as a modifier" — call this when some * other gesture (e.g. the rotary encoder's Alt+turn) consumes the hold, so - * releasing Alt afterward isn't also read as a solo tap by - * pagerKeyboardConsumeAltTap(). */ + * releasing Alt afterward does not arm the one-shot symbol layer. */ void pagerKeyboardMarkAltUsed(); -/** One-shot: true exactly once if Alt was pressed and released without being - * used as a modifier for anything else in between (no key typed, no - * pagerKeyboardMarkAltUsed() call) — a "solo tap", distinct from a - * symbol-layer or Alt+turn hold. Consumes the pending flag on read. */ -bool pagerKeyboardConsumeAltTap(); +/** Cancel one-shot/locked Alt, suppress a later release from a held Alt, and + * discard pending Alt chords. Used when input is intentionally discarded. */ +void pagerKeyboardDiscardAlt(); /** True while Backspace is physically held WITHOUT Alt (raw state, mirrors * pagerKeyboardAltHeld()). A plain press still immediately ring-pushes '\b' diff --git a/src/helpers/input/PagerKeyboardState.h b/src/helpers/input/PagerKeyboardState.h new file mode 100644 index 0000000..f2c77fc --- /dev/null +++ b/src/helpers/input/PagerKeyboardState.h @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include + +#include "LatchedModifier.h" + +class PagerKeyboardState { + public: + static constexpr uint8_t ROWS = 4; + static constexpr uint8_t COLS = 10; + static constexpr uint8_t ALT_POS = 2 * COLS; + static constexpr uint8_t SHIFT_POS = 2 * COLS + 8; + static constexpr uint8_t BACKSPACE_POS = 2 * COLS + 9; + static constexpr uint8_t SPACE_POS = 3 * COLS; + + uint8_t event(uint8_t code, bool pressed, uint32_t now_ms) { + static const char base[ROWS][COLS] = { + {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', '\r'}, + {'\0', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '\0', '\0'}, + {' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'}, + }; + static const char symbols[ROWS][COLS] = { + {'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}, + {'*', '/', '+', '-', '=', ':', '\'', '"', '@', '\0'}, + {'\0', '_', '$', ';', '?', '!', ',', '.', '\0', '\0'}, + {' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'}, + }; + + if (code == ALT_POS) { + if (pressed) alt_.press(); + else alt_.release(now_ms); + return 0; + } + if (code == SHIFT_POS) { + if (pressed) { + if (alt_.held()) { + alt_.markHeldUsed(); + alt_shift_chord_pending_ = true; + } else { + shift_held_ = true; + } + } else { + shift_held_ = false; + } + return 0; + } + if (code == BACKSPACE_POS) { + if (pressed) { + if (alt_.held()) { + alt_.markHeldUsed(); + alt_backspace_chord_pending_ = true; + } else { + alt_.consumeForKey(); + backspace_held_ = true; + return '\b'; + } + } else { + backspace_held_ = false; + } + return 0; + } + if (code == SPACE_POS) { + space_held_ = pressed; + if (pressed) { + alt_.consumeForKey(); + return ' '; + } + return 0; + } + if (!pressed) return 0; + + const uint8_t row = code / COLS; + const uint8_t col = code % COLS; + if (row >= ROWS) return 0; + const bool symbol_layer = alt_.consumeForKey(); + char key = symbol_layer ? symbols[row][col] : base[row][col]; + if (key == '\0') return 0; + if ((caps_ || shift_held_) && !symbol_layer && key >= 'a' && key <= 'z') key -= 32; + return (uint8_t)key; + } + + bool altHeld() const { return alt_.held(); } + void markAltUsed() { alt_.markHeldUsed(); } + void discardAlt() { + alt_.discard(); + alt_shift_chord_pending_ = false; + alt_backspace_chord_pending_ = false; + } + bool backspaceHeld() const { return backspace_held_; } + bool spaceHeld() const { return space_held_; } + bool consumeAltShiftChord() { + const bool pending = alt_shift_chord_pending_; + alt_shift_chord_pending_ = false; + return pending; + } + void toggleCaps() { caps_ = !caps_; } + bool consumeAltBackspaceChord() { + const bool pending = alt_backspace_chord_pending_; + alt_backspace_chord_pending_ = false; + return pending; + } + + private: + LatchedModifier alt_; + bool caps_ = false; + bool shift_held_ = false; + bool alt_shift_chord_pending_ = false; + bool alt_backspace_chord_pending_ = false; + bool backspace_held_ = false; + bool space_held_ = false; +}; \ No newline at end of file diff --git a/src/helpers/input/TDeckKeyboard.cpp b/src/helpers/input/TDeckKeyboard.cpp index 091b6b3..cdc8b47 100644 --- a/src/helpers/input/TDeckKeyboard.cpp +++ b/src/helpers/input/TDeckKeyboard.cpp @@ -2,6 +2,7 @@ #if defined(HAS_TDECK_KEYBOARD) && defined(ESP32) #include "TDeckKeyboard.h" +#include "TDeckKeyboardState.h" #include #include @@ -9,19 +10,96 @@ #define PIN_KB_ADDR 0x55 #endif -// Single-producer (core-0 poll) / single-consumer (UI thread) ring. Byte indices -// on a 32-bit MCU are atomic enough for SPSC without a lock. -static volatile uint8_t s_ring[16]; -static volatile uint8_t s_head = 0; // written by producer -static volatile uint8_t s_tail = 0; // written by consumer +// Core-0 produces and the UI task consumes. One short cross-core critical +// section publishes each byte together with its index and also protects the +// desired modifier-input mode below. +static portMUX_TYPE s_keyboard_mux = portMUX_INITIALIZER_UNLOCKED; +static uint8_t s_ring[16]; +static uint8_t s_head = 0; +static uint8_t s_tail = 0; static bool s_inited = false; +enum class KeyboardMode : uint8_t { Probe, Raw, Legacy }; +// How long to keep asking the C3 for raw frames before settling for legacy mode. +// Generous on purpose: getting this wrong costs the user modifier latching for +// the entire session, and the only cost of waiting is that the first second of +// keystrokes goes through the legacy path, which still works. +static const uint32_t kProbeWindowMs = 1500; +static KeyboardMode s_mode = KeyboardMode::Probe; +static TDeckKeyboardState s_raw_state; +static bool s_modifier_input_allowed = true; // guarded by s_keyboard_mux +static uint32_t s_modifier_mode_generation = 0; // guarded by s_keyboard_mux +static uint32_t s_modifier_generation_applied = 0; // core-0 owner only // Backlight: the UI thread requests a level; the actual I2C write happens in the // poll (core 0). The keyboard's C3 firmware sets the backlight on an I2C write. static volatile uint8_t s_bl_desired = 0; static volatile bool s_bl_dirty = false; +static void ringPushLocked(uint8_t key) { + if (!key) return; + const uint8_t next = (uint8_t)((s_head + 1) & 15); + if (next != s_tail) { + s_ring[s_head] = key; + s_head = next; + } +} + +static void keyboardCommand(uint8_t command) { + Wire.beginTransmission(PIN_KB_ADDR); + Wire.write(command); + Wire.endTransmission(); +} + +static int keyboardRead(uint8_t* out, size_t count) { + Wire.requestFrom((int)PIN_KB_ADDR, (int)count); + int read = 0; + while (Wire.available() && read < (int)count) out[read++] = (uint8_t)Wire.read(); + while (Wire.available()) Wire.read(); + return read; +} + +static void processRawFrame(const uint8_t frame[TDeckKeyboardState::COLS], uint32_t now_ms) { + uint8_t keys[16]; + portENTER_CRITICAL(&s_keyboard_mux); + const uint32_t generation = s_modifier_mode_generation; + if (generation != s_modifier_generation_applied) { + // Transition frame: establish current physical state, publish nothing. + // The mode setter already cleared older queued bytes under this same lock. + s_raw_state.baseline(frame); + s_modifier_generation_applied = generation; + } else { + const size_t key_count = s_raw_state.update(frame, now_ms, keys, sizeof keys, + s_modifier_input_allowed); + for (size_t i = 0; i < key_count; ++i) ringPushLocked(keys[i]); + } + portEXIT_CRITICAL(&s_keyboard_mux); +} + +static void processLegacyKey(uint8_t key) { + portENTER_CRITICAL(&s_keyboard_mux); + const uint32_t generation = s_modifier_mode_generation; + if (generation != s_modifier_generation_applied) { + // No raw modifier state exists, and the C3 exposes one latest-byte mailbox + // (comdata/comdata_flag), not one response per host mode transition. Any + // number of transitions before this read therefore coalesce deliberately: + // drop the sole pending byte once so it cannot cross into the final mode. + s_modifier_generation_applied = generation; + } else { + ringPushLocked(key); + } + portEXIT_CRITICAL(&s_keyboard_mux); +} + void tdeckKeyboardBegin() { + s_mode = KeyboardMode::Probe; + s_raw_state = TDeckKeyboardState{}; + portENTER_CRITICAL(&s_keyboard_mux); + s_head = s_tail = 0; + // Keep any desired mode the UI published immediately after starting this + // task. Setting applied one generation behind forces the first response to + // baseline (or drain the legacy controller's one-byte mailbox). + s_modifier_generation_applied = s_modifier_mode_generation - 1; + portEXIT_CRITICAL(&s_keyboard_mux); s_inited = true; // Wire was configured (18/8, 400k, 20ms timeout) by the touch driver } @@ -49,21 +127,92 @@ void tdeckKeyboardFlushBacklight() { void tdeckKeyboardPoll() { if (!s_inited) return; tdeckKeyboardFlushBacklight(); - if (Wire.requestFrom((int)PIN_KB_ADDR, 1) != 1) return; - uint8_t key = Wire.read(); - if (key == 0) return; // no key this scan - const uint8_t nh = (uint8_t)((s_head + 1) & 15); - if (nh != s_tail) { // drop if the ring is full - s_ring[s_head] = key; - s_head = nh; + if (s_mode == KeyboardMode::Probe) { + // LilyGO keyboard firmware from June 2025 onward accepts 0x03 and returns + // five column-state bytes. Older controllers ignore it and keep returning + // one resolved ASCII byte; restore 0x04 key mode when that is what we have. + // + // KEEP RETRYING before concluding "old firmware". This used to decide on a + // SINGLE read, and the first I2C read after boot is the least trustworthy + // one there is: the C3 is still coming up, so a short or garbled frame is + // ordinary. One such frame committed the device to legacy mode for the whole + // session, with no recovery short of a reboot -- reported as latching simply + // not working on a T-Deck carrying the June 2025 firmware (#332). + static uint32_t s_probe_start_ms = 0; + const uint32_t now = millis(); + if (!s_probe_start_ms) s_probe_start_ms = now; + + keyboardCommand(0x03); + uint8_t frame[TDeckKeyboardState::COLS] = {}; + const int count = keyboardRead(frame, sizeof frame); + bool valid_raw = count == (int)sizeof frame; + size_t bad_col = 0; + for (size_t col = 0; col < sizeof frame && valid_raw; ++col) { + if (frame[col] & 0x80) { valid_raw = false; bad_col = col; } + } + if (valid_raw) { + s_mode = KeyboardMode::Raw; + Serial.println("[keyboard] T-Deck raw mode: modifier latching enabled"); + processRawFrame(frame, millis()); + return; + } + // A legacy controller answers with one resolved byte. Deliver it either way + // so nothing typed during the probe window is dropped. + if (count == 1) { keyboardCommand(0x04); processLegacyKey(frame[0]); } + if ((uint32_t)(now - s_probe_start_ms) < kProbeWindowMs) return; // try again next poll + + keyboardCommand(0x04); + s_mode = KeyboardMode::Legacy; + // Say WHY, so the next report of "latching does not work" is one line to + // diagnose instead of a guess about which firmware someone has. + Serial.printf("[keyboard] T-Deck legacy mode: update keyboard C3 firmware for modifier " + "latching (probe read %d bytes, wanted %d%s)\n", + count, (int)sizeof frame, + (count == (int)sizeof frame) ? ", high bit set in a column" : ""); + (void)bad_col; + return; } + if (s_mode == KeyboardMode::Raw) { + uint8_t frame[TDeckKeyboardState::COLS] = {}; + if (keyboardRead(frame, sizeof frame) != (int)sizeof frame) return; + for (size_t col = 0; col < sizeof frame; ++col) if (frame[col] & 0x80) return; + processRawFrame(frame, millis()); + return; + } + uint8_t key = 0; + if (keyboardRead(&key, 1) == 1) processLegacyKey(key); } int tdeckKeyboardReadKey() { - if (s_tail == s_head) return 0; // empty + portENTER_CRITICAL(&s_keyboard_mux); + if (s_tail == s_head) { + portEXIT_CRITICAL(&s_keyboard_mux); + return 0; + } const uint8_t key = s_ring[s_tail]; s_tail = (uint8_t)((s_tail + 1) & 15); + portEXIT_CRITICAL(&s_keyboard_mux); return key; } +void tdeckKeyboardDiscardModifiers() { + portENTER_CRITICAL(&s_keyboard_mux); + if (s_modifier_input_allowed) { + s_modifier_input_allowed = false; + ++s_modifier_mode_generation; + s_tail = s_head; + } + portEXIT_CRITICAL(&s_keyboard_mux); +} + +void tdeckKeyboardAllowModifiers() { + portENTER_CRITICAL(&s_keyboard_mux); + if (!s_modifier_input_allowed) { + s_modifier_input_allowed = true; + ++s_modifier_mode_generation; + s_tail = s_head; + } + portEXIT_CRITICAL(&s_keyboard_mux); +} + #endif diff --git a/src/helpers/input/TDeckKeyboard.h b/src/helpers/input/TDeckKeyboard.h index cf64910..4030baf 100644 --- a/src/helpers/input/TDeckKeyboard.h +++ b/src/helpers/input/TDeckKeyboard.h @@ -1,14 +1,16 @@ #pragma once // LilyGo T-Deck physical keyboard. It's a separate ESP32-C3 running the -// T-Keyboard firmware on the shared I2C bus (SDA 18 / SCL 8, addr 0x55): each -// 1-byte read returns the ASCII code of the last key press (0 = none). The C3 -// resolves shift / sym layers itself, so we just receive final characters. +// T-Keyboard firmware on the shared I2C bus (SDA 18 / SCL 8, addr 0x55). +// Current controller firmware supports a five-byte raw matrix mode, which lets +// this driver preserve held modifiers and add tap/double-tap latching. Older +// firmware returns one resolved ASCII byte; startup detects that and restores +// the original controller-side key mode automatically. // // CRITICAL: the keyboard shares the I2C bus with the GT911 touch controller, // which is polled from a core-0 task. To avoid two cores hitting Wire at once, // tdeckKeyboardPoll() must be called from THAT task; the UI thread only drains -// the lock-free ring via tdeckKeyboardReadKey(). +// the critical-section-protected ring via tdeckKeyboardReadKey(). #if defined(HAS_TDECK_KEYBOARD) && defined(ESP32) #include @@ -23,6 +25,14 @@ void tdeckKeyboardPoll(); /** Pop the next buffered key (ASCII), or 0 if none. Safe from the UI thread. */ int tdeckKeyboardReadKey(); +/** Cancel one-shot/locked modifiers and suppress a later release from a + * currently held modifier. Call when queued input is intentionally discarded. */ +void tdeckKeyboardDiscardModifiers(); + +/** Stop continuously discarding raw modifier state after the UI returns to a + * mode where keyboard input is meaningful. */ +void tdeckKeyboardAllowModifiers(); + /** Request a keyboard-backlight level (0 = off, 0xFF = on). Safe from the UI * thread — the I2C write happens inside tdeckKeyboardPoll() (core 0), which * owns the bus. Only re-sent when the value changes. */ diff --git a/src/helpers/input/TDeckKeyboardState.h b/src/helpers/input/TDeckKeyboardState.h new file mode 100644 index 0000000..3848046 --- /dev/null +++ b/src/helpers/input/TDeckKeyboardState.h @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#include +#include + +#include "LatchedModifier.h" + +class TDeckKeyboardState { + public: + static constexpr size_t COLS = 5; + + size_t update(const uint8_t current[COLS], uint32_t now_ms, + uint8_t* out, size_t out_capacity, bool modifiers_enabled = true) { + static const char base[COLS][7] = { + {'q', 'w', '\0', 'a', '\0', ' ', '\0'}, + {'e', 's', 'd', 'p', 'x', 'z', '\0'}, + {'r', 'g', 't', '\0', 'v', 'c', 'f'}, + {'u', 'h', 'y', '\0', 'b', 'n', 'j'}, + {'o', 'l', 'i', '\0', '$', 'm', 'k'}, + }; + static const char symbols[COLS][7] = { + {'#', '1', '\0', '*', '\0', '\0', '0'}, + {'2', '4', '5', '@', '8', '7', '\0'}, + {'3', '/', '(', '\0', '?', '9', '6'}, + {'_', ':', ')', '\0', '!', ',', ';'}, + {'+', '"', '-', '\0', '\0', '.', '\''}, + }; + auto down = [&](size_t col, uint8_t row) { + return (current[col] & (uint8_t)(1U << row)) != 0; + }; + auto pressed = [&](size_t col, uint8_t row) { + return down(col, row) && (previous_[col] & (uint8_t)(1U << row)) == 0; + }; + auto released = [&](size_t col, uint8_t row) { + return !down(col, row) && (previous_[col] & (uint8_t)(1U << row)) != 0; + }; + + if (!modifiers_enabled) { + symbol_.baseline(down(0, 2)); + alt_.baseline(down(0, 4)); + } + if (modifiers_enabled && pressed(0, 2)) symbol_.press(); + if (modifiers_enabled && pressed(0, 4)) alt_.press(); + if (modifiers_enabled && released(0, 2)) symbol_.release(now_ms); + if (modifiers_enabled && released(0, 4)) alt_.release(now_ms); + + const bool shift = modifiers_enabled && (down(1, 6) || down(2, 3)); + size_t written = 0; + auto emit = [&](uint8_t key) { + if (written < out_capacity) out[written++] = key; + }; + for (uint8_t row = 0; row < 7; ++row) { + for (size_t col = 0; col < COLS; ++col) { + if (!pressed(col, row)) continue; + if ((col == 0 && (row == 2 || row == 4)) || + (col == 1 && row == 6) || (col == 2 && row == 3)) continue; + const bool physical_alt = modifiers_enabled && alt_.held(); + if (physical_alt) alt_.markHeldUsed(); + // Preserve LilyGO controller shortcuts while raw mode moves ordinary + // matrix translation into the host. The C3 still handles Alt+B's + // backlight toggle; suppress its character exactly as key mode does. + if (physical_alt && col == 3 && row == 4) { + symbol_.consumeForKey(); + continue; + } + if (physical_alt && col == 2 && row == 5) { + symbol_.consumeForKey(); + emit(0x0C); + continue; + } + if (col == 3 && row == 3) { + symbol_.consumeForKey(); alt_.consumeForKey(); emit('\r'); continue; + } + if (col == 4 && row == 3) { + symbol_.consumeForKey(); alt_.consumeForKey(); emit('\b'); continue; + } + + const bool use_symbols = modifiers_enabled && (symbol_.active() || alt_.latched()); + const char key = use_symbols ? symbols[col][row] : base[col][row]; + if (modifiers_enabled) { + symbol_.consumeForKey(); + if (!physical_alt) alt_.consumeForKey(); + } + if (key == '\0') continue; + char resolved = key; + if (!use_symbols && shift && resolved >= 'a' && resolved <= 'z') resolved -= 32; + emit((uint8_t)resolved); + } + } + + for (size_t col = 0; col < COLS; ++col) previous_[col] = current[col] & 0x7F; + return written; + } + + void baseline(const uint8_t current[COLS]) { + symbol_.baseline((current[0] & (uint8_t)(1U << 2)) != 0); + alt_.baseline((current[0] & (uint8_t)(1U << 4)) != 0); + for (size_t col = 0; col < COLS; ++col) previous_[col] = current[col] & 0x7F; + } + + void discardModifiers() { + symbol_.discard(); + alt_.discard(); + } + + private: + uint8_t previous_[COLS] = {}; + LatchedModifier symbol_; + LatchedModifier alt_; +}; \ No newline at end of file diff --git a/src/ui-touch/LuaAppHost.cpp b/src/ui-touch/LuaAppHost.cpp index dd51781..efad828 100644 --- a/src/ui-touch/LuaAppHost.cpp +++ b/src/ui-touch/LuaAppHost.cpp @@ -18,6 +18,7 @@ #include "AppPage.h" #include "device_caps.h" #include "i18n.h" // wada.sys.tr: apps can use the same translation table the UI does +#include "Utf8Text.h" #include "helpers/esp32/WdtHeavyGuard.h" // wada.fs writes can trigger SPIFFS GC extern "C" { @@ -35,6 +36,18 @@ extern bool luaHostScreenOn(); // false = display asleep; app tick extern void luaHostKeepAwake(bool on); // hold the screen + ticks for a measuring app // notification chime; false = no sounder / muted extern fs::FS* luaHostAppFs(); // /apps storage root FS (may be null) extern void luaHostAppPath(char* out, size_t cap, const char* rel); // prefixes the store root +#if CAP_LUA_AUDIO +extern bool luaHostAudioPlay(fs::FS* fs, const char* path, const char* shown, + const char* source, uint32_t owner, + char* error, size_t error_cap); +extern bool luaHostAudioPause(uint32_t owner, bool pause); +extern bool luaHostAudioStop(uint32_t owner, bool release); +extern void luaHostAudioStatus(uint32_t owner, char* state, size_t state_cap, + char* path, size_t path_cap, + char* source, size_t source_cap, + char* format, size_t format_cap, + char* error, size_t error_cap); +#endif #if CAP_LUA_SD_LIST extern fs::FS* luaHostSdFs(bool* busy); // mounted physical SD, or null extern bool luaHostSdReadFailed(); // failed open was a dead card @@ -157,10 +170,25 @@ void budgetHook(lua_State* L, lua_Debug*) { // host state // --------------------------------------------------------------------------- constexpr size_t kHeapCap = 256 * 1024; // per-app PSRAM cap -constexpr size_t kMaxSrc = 64 * 1024; // app source size limit +// App source size limit. Was 64 KB, which a real app can reach: reported from a +// 2000-contact device whose author hit it writing an ordinary Lua program, not +// anything pathological. The source lives in PSRAM only until luaL_loadbuffer +// has compiled it, so this is a transient allocation rather than a per-app cost, +// and the compiled chunk still has to fit the 256 KB per-app heap below. +constexpr size_t kMaxSrc = 192 * 1024; // app source size limit constexpr size_t kStoreMax = 2048; // per-app persisted KV budget (bytes, serialized) constexpr int kMinTickMs = 33; // fastest on_tick cadence (~30 fps) +// Bumped by wada.ui.clear(). Every widget handle records the generation it was +// created in; a handle from an older one has already been destroyed, so the +// accessors below null its pointer and the `if (u->obj)` guard that every +// method already has does the rest. +// +// Without this, clear() would be a use-after-free generator: WidgetUd holds a +// raw lv_obj_t*, so a Lua variable still referring to a label from the previous +// screen would sail past the null check straight into freed memory. +static uint32_t s_ui_gen = 1; + struct Host { lua_State* L = nullptr; LuaHeap heap; @@ -184,10 +212,12 @@ struct Host { AppTimer timers[kMaxTimers]; bool in_lua = false; // re-entrancy guard (dismiss from inside a callback) bool want_close = false; + uint32_t generation = 0; // resource ownership across close/reopen char id[24] = ""; char title[32] = ""; }; Host* s_h = nullptr; +uint32_t s_host_generation = 0; char s_bar_title[40]; // appPageBegin keeps the pointer — must outlive the page @@ -246,8 +276,21 @@ uint32_t argColor(lua_State* L, int idx, uint32_t def = 0xFFFFFF) { return (uint32_t)luaL_optinteger(L, idx, (lua_Integer)def) & 0xFFFFFF; } +// LVGL assumes structurally valid UTF-8 and can stop advancing when an app +// hands it a partial codepoint. Keep one reusable repair buffer: valid strings +// stay zero-copy, while malformed runs become one ASCII '?'. Every call below +// is synchronous and LVGL copies label text before this buffer can be reused. +const char* safeUiText(const char* text, size_t length, size_t* safe_length = nullptr, + unsigned scratch_slot = 0) { + static std::string scratch_slots[2]; + std::string& scratch = scratch_slots[scratch_slot & 1U]; + const char* safe = Utf8Text::sanitize(text, length, scratch); + if (safe_length) *safe_length = safe == text ? length : scratch.size(); + return safe; +} + // ---- canvas userdata ---- -struct CanvasUd { lv_obj_t* obj; lv_color_t* buf; int w, h; }; +struct CanvasUd { lv_obj_t* obj; lv_color_t* buf; int w, h; uint32_t gen; }; CanvasUd* checkCanvas(lua_State* L) { return (CanvasUd*)luaL_checkudata(L, 1, "wada.canvas"); @@ -298,11 +341,14 @@ int cvCircle(lua_State* L) { int cvText(lua_State* L) { CanvasUd* c = checkCanvas(L); if (!c->obj) return 0; + const lv_coord_t x = (lv_coord_t)luaL_checkinteger(L, 2); + const lv_coord_t y = (lv_coord_t)luaL_checkinteger(L, 3); + size_t text_length = 0; + const char* text = luaL_checklstring(L, 4, &text_length); lv_draw_label_dsc_t d; lv_draw_label_dsc_init(&d); d.color = lv_color_hex(argColor(L, 5)); d.font = luaHostFontForSize((int)luaL_optinteger(L, 6, 14)); - lv_canvas_draw_text(c->obj, (lv_coord_t)luaL_checkinteger(L, 2), (lv_coord_t)luaL_checkinteger(L, 3), - c->w, &d, luaL_checkstring(L, 4)); + lv_canvas_draw_text(c->obj, x, y, c->w, &d, safeUiText(text, text_length)); return 0; } int cvPos(lua_State* L) { @@ -337,6 +383,7 @@ int uiCanvas(lua_State* L) { lv_canvas_set_buffer(cv, buf, w, h, LV_IMG_CF_TRUE_COLOR); lv_canvas_fill_bg(cv, lv_color_hex(0x000000), LV_OPA_COVER); CanvasUd* ud = (CanvasUd*)lua_newuserdatauv(L, sizeof(CanvasUd), 0); + ud->gen = s_ui_gen; ud->obj = cv; ud->buf = buf; ud->w = w; ud->h = h; luaL_setmetatable(L, "wada.canvas"); // Pin the handle in the registry for the app's lifetime. LVGL draws straight @@ -352,12 +399,18 @@ int uiCanvas(lua_State* L) { } // ---- label userdata ---- -struct WidgetUd { lv_obj_t* obj; }; -WidgetUd* checkLabel(lua_State* L) { return (WidgetUd*)luaL_checkudata(L, 1, "wada.label"); } +struct WidgetUd { lv_obj_t* obj; uint32_t gen; }; +WidgetUd* checkLabel(lua_State* L) { + WidgetUd* u = (WidgetUd*)luaL_checkudata(L, 1, "wada.label"); + if (u->gen != s_ui_gen) u->obj = nullptr; // cleared out from under this handle + return u; +} int lbSet(lua_State* L) { WidgetUd* u = checkLabel(L); - if (u->obj) lv_label_set_text(u->obj, luaL_checkstring(L, 2)); + size_t length = 0; + const char* text = luaL_checklstring(L, 2, &length); + if (u->obj) lv_label_set_text(u->obj, safeUiText(text, length)); return 0; } int lbPos(lua_State* L) { @@ -390,20 +443,31 @@ int lbWidth(lua_State* L) { // label:width(px [, "left"|"center"|"right"]) — int uiLabel(lua_State* L) { if (!s_h || !s_h->body) return luaL_error(L, "no app body"); + size_t length = 0; + const char* text = luaL_checklstring(L, 1, &length); + const lv_coord_t x = (lv_coord_t)luaL_optinteger(L, 2, 0); + const lv_coord_t y = (lv_coord_t)luaL_optinteger(L, 3, 0); + const lv_font_t* font = luaHostFontForSize((int)luaL_optinteger(L, 4, 14)); + const uint32_t color = argColor(L, 5, 0xE6E9ED); lv_obj_t* l = lv_label_create(s_h->body); - lv_label_set_text(l, luaL_checkstring(L, 1)); - lv_obj_set_pos(l, (lv_coord_t)luaL_optinteger(L, 2, 0), (lv_coord_t)luaL_optinteger(L, 3, 0)); - lv_obj_set_style_text_font(l, luaHostFontForSize((int)luaL_optinteger(L, 4, 14)), LV_PART_MAIN); - lv_obj_set_style_text_color(l, lv_color_hex(argColor(L, 5, 0xE6E9ED)), LV_PART_MAIN); + lv_label_set_text(l, safeUiText(text, length)); + lv_obj_set_pos(l, x, y); + lv_obj_set_style_text_font(l, font, LV_PART_MAIN); + lv_obj_set_style_text_color(l, lv_color_hex(color), LV_PART_MAIN); WidgetUd* ud = (WidgetUd*)lua_newuserdatauv(L, sizeof(WidgetUd), 0); + ud->gen = s_ui_gen; ud->obj = l; luaL_setmetatable(L, "wada.label"); return 1; } // ---- chart userdata (the primitive the native Monitor/Airtime pages use) ---- -struct ChartUd { lv_obj_t* obj; lv_chart_series_t* ser[2]; int n_ser; }; -ChartUd* checkChart(lua_State* L) { return (ChartUd*)luaL_checkudata(L, 1, "wada.chart"); } +struct ChartUd { lv_obj_t* obj; lv_chart_series_t* ser[2]; int n_ser; uint32_t gen; }; +ChartUd* checkChart(lua_State* L) { + ChartUd* u = (ChartUd*)luaL_checkudata(L, 1, "wada.chart"); + if (u->gen != s_ui_gen) { u->obj = nullptr; u->n_ser = 0; } + return u; +} int chPush(lua_State* L) { // chart:push(series_idx, value) ChartUd* c = checkChart(L); @@ -470,6 +534,7 @@ int uiChart(lua_State* L) { // wada.ui.chart(w, h, points, color1 [, color2] [ lv_obj_set_style_line_color(ch, lv_color_hex(0x1A1D1F), LV_PART_MAIN); // grid lv_obj_set_style_line_width(ch, 2, LV_PART_ITEMS); ChartUd* ud = (ChartUd*)lua_newuserdatauv(L, sizeof(ChartUd), 0); + ud->gen = s_ui_gen; ud->obj = ch; ud->n_ser = 0; ud->ser[0] = lv_chart_add_series(ch, lv_color_hex(argColor(L, 4, 0x15B6A6)), LV_CHART_AXIS_PRIMARY_Y); @@ -497,12 +562,17 @@ void btnEventCb(lv_event_t* e) { int uiButton(lua_State* L) { if (!s_h || !s_h->body) return luaL_error(L, "no app body"); - const char* txt = luaL_checkstring(L, 1); + size_t text_length = 0; + const char* text = luaL_checklstring(L, 1, &text_length); + const lv_coord_t x = (lv_coord_t)luaL_checkinteger(L, 2); + const lv_coord_t y = (lv_coord_t)luaL_checkinteger(L, 3); + const lv_coord_t w = (lv_coord_t)luaL_optinteger(L, 4, 90); + const lv_coord_t h = (lv_coord_t)luaL_optinteger(L, 5, 34); lv_obj_t* b = lv_btn_create(s_h->body); - lv_obj_set_pos(b, (lv_coord_t)luaL_checkinteger(L, 2), (lv_coord_t)luaL_checkinteger(L, 3)); - lv_obj_set_size(b, (lv_coord_t)luaL_optinteger(L, 4, 90), (lv_coord_t)luaL_optinteger(L, 5, 34)); + lv_obj_set_pos(b, x, y); + lv_obj_set_size(b, w, h); lv_obj_t* bl = lv_label_create(b); - lv_label_set_text(bl, txt); + lv_label_set_text(bl, safeUiText(text, text_length)); lv_obj_center(bl); if (lua_isfunction(L, 6)) { lua_rawgeti(L, LUA_REGISTRYINDEX, s_h->ref_btncb); @@ -513,6 +583,7 @@ int uiButton(lua_State* L) { lv_obj_add_event_cb(b, btnEventCb, LV_EVENT_CLICKED, nullptr); } WidgetUd* ud = (WidgetUd*)lua_newuserdatauv(L, sizeof(WidgetUd), 0); + ud->gen = s_ui_gen; ud->obj = b; luaL_setmetatable(L, "wada.label"); // shares set/pos/color methods return 1; @@ -529,8 +600,12 @@ int uiButton(lua_State* L) { // keyboard/trackball focus navigation walks them on touchless boards for free, // and a tap works on the ones with a screen. Per-row callbacks ride the same // button-callback table uiButton uses. -struct ListUd { lv_obj_t* obj; int sel; }; -ListUd* checkList(lua_State* L) { return (ListUd*)luaL_checkudata(L, 1, "wada.list"); } +struct ListUd { lv_obj_t* obj; int sel; uint32_t gen; }; +ListUd* checkList(lua_State* L) { + ListUd* u = (ListUd*)luaL_checkudata(L, 1, "wada.list"); + if (u->gen != s_ui_gen) u->obj = nullptr; + return u; +} lv_obj_t* listRowAt(ListUd* u, int i) { // i is 1-based, as everywhere in Lua if (!u->obj || i < 1) return nullptr; @@ -554,6 +629,7 @@ int uiTextW(lua_State* L) { size_t len = 0; const char* txt = luaL_checklstring(L, 1, &len); const lv_font_t* f = luaHostFontForSize((int)luaL_optinteger(L, 2, 14)); + txt = safeUiText(txt, len, &len); lua_pushinteger(L, (lua_Integer)lv_txt_get_width(txt, (uint32_t)len, f, 0, LV_TEXT_FLAG_NONE)); return 1; } @@ -566,6 +642,7 @@ int uiTextLines(lua_State* L) { const char* txt = luaL_checklstring(L, 1, &len); const int w = (int)luaL_checkinteger(L, 2); const lv_font_t* f = luaHostFontForSize((int)luaL_optinteger(L, 3, 14)); + txt = safeUiText(txt, len, &len); if (w <= 0) { lua_pushinteger(L, 1); return 1; } int lines = 0; const char* p = txt; @@ -580,6 +657,24 @@ int uiTextLines(lua_State* L) { return 1; } +// wada.ui.clear() -- remove every widget from the app's page. +// +// The SDK documented building widgets in on_open() and gave no way to take them +// down again, so an app with more than one screen drew the second on top of the +// first. Reported by pisti87, who had been working around it with buttons. +// +// Bumping the generation is what makes this safe. A Lua variable still holding a +// label from the screen just cleared keeps a raw pointer to freed memory; the +// accessors compare generations and null it, so the existing `if (u->obj)` guard +// in every method turns a stale call into a no-op instead of a crash. +static int uiClear(lua_State* L) { + (void)L; + if (!s_h || !s_h->body) return 0; + lv_obj_clean(s_h->body); // deletes the children, keeps the page itself + ++s_ui_gen; + return 0; +} + int uiList(lua_State* L) { if (!s_h || !s_h->body) return luaL_error(L, "no app body"); const int x = (int)luaL_checkinteger(L, 1), y = (int)luaL_checkinteger(L, 2); @@ -597,6 +692,7 @@ int uiList(lua_State* L) { lv_obj_set_scroll_dir(c, LV_DIR_VER); lv_obj_add_flag(c, LV_OBJ_FLAG_SCROLLABLE); ListUd* ud = (ListUd*)lua_newuserdatauv(L, sizeof(ListUd), 0); + ud->gen = s_ui_gen; ud->obj = c; ud->sel = 0; luaL_setmetatable(L, "wada.list"); return 1; @@ -605,7 +701,8 @@ int uiList(lua_State* L) { // list:add(text [, fn]) -> index of the new row int lsAdd(lua_State* L) { ListUd* u = checkList(L); - const char* txt = luaL_checkstring(L, 2); + size_t text_length = 0; + const char* text = luaL_checklstring(L, 2, &text_length); if (!u->obj || !s_h) return 0; lv_obj_t* row = lv_btn_create(u->obj); lv_obj_set_width(row, LV_PCT(100)); @@ -614,7 +711,7 @@ int lsAdd(lua_State* L) { lv_obj_set_style_pad_all(row, 6, LV_PART_MAIN); listPaintRow(row, false); lv_obj_t* lb = lv_label_create(row); - lv_label_set_text(lb, txt); + lv_label_set_text(lb, safeUiText(text, text_length)); lv_label_set_long_mode(lb, LV_LABEL_LONG_DOT); // a long name truncates instead of reflowing the row lv_obj_set_width(lb, LV_PCT(100)); lv_obj_set_style_text_font(lb, luaHostFontForSize(12), LV_PART_MAIN); @@ -633,9 +730,11 @@ int lsAdd(lua_State* L) { int lsSet(lua_State* L) { ListUd* u = checkList(L); lv_obj_t* row = listRowAt(u, (int)luaL_checkinteger(L, 2)); + size_t text_length = 0; + const char* text = luaL_checklstring(L, 3, &text_length); if (!row) return 0; lv_obj_t* lb = lv_obj_get_child(row, 0); - if (lb) lv_label_set_text(lb, luaL_checkstring(L, 3)); + if (lb) lv_label_set_text(lb, safeUiText(text, text_length)); return 0; } int lsColor(lua_State* L) { @@ -764,12 +863,17 @@ int sysBeep(lua_State* L) { lua_pushboolean(L, luaHostBeep()); return 1; } // rather than assume: the extended SDK is absent on low-resource boards (see // CAP_LUA_SDK_EXT in device_caps.h), and a store app runs on all of them. int sysCaps(lua_State* L) { - lua_createtable(L, 0, 14); + lua_createtable(L, 0, 20); lua_pushboolean(L, CAP_LUA_SDK_EXT); lua_setfield(L, -2, "sdk_ext"); lua_pushboolean(L, CAP_KEYBOARD); lua_setfield(L, -2, "keyboard"); lua_pushboolean(L, CAP_TOUCH); lua_setfield(L, -2, "touch"); lua_pushboolean(L, CAP_SD); lua_setfield(L, -2, "sd"); lua_pushboolean(L, CAP_LUA_SD_LIST); lua_setfield(L, -2, "sd_list"); + lua_pushboolean(L, CAP_LUA_AUDIO); lua_setfield(L, -2, "audio"); + lua_pushboolean(L, CAP_LUA_AUDIO); lua_setfield(L, -2, "audio_wav"); + lua_pushboolean(L, CAP_LUA_AUDIO); lua_setfield(L, -2, "audio_mp3"); + lua_pushboolean(L, CAP_LUA_AUDIO && CAP_LUA_SD_LIST); + lua_setfield(L, -2, "audio_sd"); // Feature flags for the calls added after the first extended SDK shipped, so // an app can degrade instead of erroring on firmware that predates them. lua_pushboolean(L, CAP_LUA_SDK_EXT); lua_setfield(L, -2, "discover"); // wada.mesh.discover @@ -1563,6 +1667,93 @@ int sdList(lua_State* L) { #endif #endif // CAP_LUA_SDK_EXT +#if CAP_LUA_AUDIO +static bool audioSafeName(const char* name, size_t len) { + if (!name || len == 0 || len > 32 || name[0] == '.') return false; + for (size_t i = 0; i < len; ++i) { + const char c = name[i]; + const bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-'; + if (!ok) return false; + } + return true; +} + +static int audioError(lua_State* L, const char* error) { + lua_pushnil(L); + lua_pushstring(L, error); + return 2; +} + +// wada.audio.play(name) resolves inside /apps/.d on the board's active +// storage backend. An explicit sd:/... path is available only with sd_list. +int audioPlay(lua_State* L) { + if (!s_h) return audioError(L, "closed"); + size_t requested_len = 0; + const char* requested = luaL_checklstring(L, 1, &requested_len); + fs::FS* fs = nullptr; + char path[224] = ""; + const char* source = "app"; + + if (requested_len >= 3 && !memcmp(requested, "sd:", 3)) { +#if CAP_LUA_SD_LIST + const char* card_path = requested + 3; + const size_t card_len = requested_len - 3; + if (!sdSafePath(card_path, card_len, path, sizeof path)) return audioError(L, "bad path"); + bool busy = false; + fs = luaHostSdFs(&busy); + if (!fs) return audioError(L, busy ? "busy" : "no sd"); + source = "sd"; +#else + return audioError(L, "no sd"); +#endif + } else { + if (!audioSafeName(requested, requested_len)) return audioError(L, "bad path"); + fs = luaHostAppFs(); + if (!fs) return audioError(L, "no storage"); + char rel[96]; + snprintf(rel, sizeof rel, "/apps/%s.d/%s", s_h->id, requested); + luaHostAppPath(path, sizeof path, rel); + } + + char error[40] = ""; + if (!luaHostAudioPlay(fs, path, requested, source, s_h->generation, error, sizeof error)) + return audioError(L, error[0] ? error : "playback failed"); + lua_pushboolean(L, 1); + return 1; +} + +int audioPause(lua_State* L) { + lua_pushboolean(L, s_h && luaHostAudioPause(s_h->generation, true)); + return 1; +} + +int audioResume(lua_State* L) { + lua_pushboolean(L, s_h && luaHostAudioPause(s_h->generation, false)); + return 1; +} + +int audioStop(lua_State* L) { + lua_pushboolean(L, s_h && luaHostAudioStop(s_h->generation, false)); + return 1; +} + +int audioStatus(lua_State* L) { + char state[12], path[192], source[8], format[8], error[40]; + luaHostAudioStatus(s_h ? s_h->generation : 0, + state, sizeof state, path, sizeof path, + source, sizeof source, format, sizeof format, + error, sizeof error); + lua_createtable(L, 0, 5); + lua_pushstring(L, state); lua_setfield(L, -2, "state"); + lua_pushstring(L, path); lua_setfield(L, -2, "path"); + lua_pushstring(L, source); lua_setfield(L, -2, "source"); + lua_pushstring(L, format); lua_setfield(L, -2, "format"); + if (error[0]) { lua_pushstring(L, error); lua_setfield(L, -2, "error"); } + return 1; +} +#endif + void storePath(char* out, size_t cap) { char rel[40]; snprintf(rel, sizeof rel, "/apps/%s.sav", s_h->id); @@ -1694,10 +1885,35 @@ void pressCb(lv_event_t* e) { } // ---- wada.mesh (read-only) ---- +// wada.mesh.contacts([offset], [limit]) +// +// Used to scan the first 200 contacts and return at most 100, with no way to see +// past that and nothing to say it had truncated. On a device with 2000 contacts +// an app simply could not reach most of them (reported by Jade). +// +// It takes an offset and a limit now. The default limit stays 100 because every +// entry is an 8-field Lua table and building thousands of them in one call would +// blow the per-app heap -- but the window can be moved, so all contacts are +// reachable by asking twice. wada.mesh.contact_count() says how many there are. +// How many contacts the device holds, so an app can page through them without +// calling contacts() repeatedly just to discover where the list ends. +static int meshContactCount(lua_State* L) { + char name[36], pk[12]; int type; uint32_t ago; double lat, lon; int32_t lat6, lon6; + int n = 0; + while (n < 4096 && luaHostContactAt(n, name, sizeof name, &type, &ago, &lat, &lon, + pk, sizeof pk, &lat6, &lon6)) ++n; + lua_pushinteger(L, n); + return 1; +} + int meshContacts(lua_State* L) { + const int offset = (int)luaL_optinteger(L, 1, 0); + int limit = (int)luaL_optinteger(L, 2, 100); + if (limit < 1) limit = 1; + if (limit > 250) limit = 250; // one call still has to fit the app's heap lua_newtable(L); char name[36], pk[12]; int type; uint32_t ago; double lat, lon; int32_t lat6, lon6; - for (int i = 0, out = 0; i < 200 && out < 100; i++) { + for (int i = (offset > 0 ? offset : 0), out = 0; out < limit; i++) { if (!luaHostContactAt(i, name, sizeof name, &type, &ago, &lat, &lon, pk, sizeof pk, &lat6, &lon6)) break; lua_createtable(L, 0, 8); @@ -1939,11 +2155,14 @@ void promptDeliver(const char* text) { } int uiInput(lua_State* L) { - const char* title = luaL_optstring(L, 1, ""); - const char* initial = luaL_optstring(L, 2, ""); + size_t title_length = 0, initial_length = 0; + const char* title_raw = luaL_optlstring(L, 1, "", &title_length); + const char* initial_raw = luaL_optlstring(L, 2, "", &initial_length); luaL_checktype(L, 3, LUA_TFUNCTION); if (!s_h) return luaL_error(L, "no app"); if (s_h->prompt_cb != LUA_NOREF) return luaL_error(L, "an input prompt is already open"); + const char* title = safeUiText(title_raw, title_length, nullptr, 0); + const char* initial = safeUiText(initial_raw, initial_length, nullptr, 1); lua_pushvalue(L, 3); s_h->prompt_cb = luaL_ref(L, LUA_REGISTRYINDEX); luaHostTextPrompt(title, initial, promptDeliver); @@ -2105,6 +2324,7 @@ void openWada(lua_State* L) { lua_setfield(L, -2, "colors"); lua_pushcfunction(L, uiInput); lua_setfield(L, -2, "input"); // modal text entry lua_pushcfunction(L, uiList); lua_setfield(L, -2, "list"); // scrollable selectable rows + lua_pushcfunction(L, uiClear); lua_setfield(L, -2, "clear"); // wipe the page (#318 / Discord) lua_setfield(L, -2, "ui"); lua_newtable(L); // wada.sys @@ -2172,8 +2392,19 @@ void openWada(lua_State* L) { lua_setfield(L, -2, "sd"); #endif +#if CAP_LUA_AUDIO + lua_newtable(L); // wada.audio (app storage + optional SD) + lua_pushcfunction(L, audioPlay); lua_setfield(L, -2, "play"); + lua_pushcfunction(L, audioPause); lua_setfield(L, -2, "pause"); + lua_pushcfunction(L, audioResume); lua_setfield(L, -2, "resume"); + lua_pushcfunction(L, audioStop); lua_setfield(L, -2, "stop"); + lua_pushcfunction(L, audioStatus); lua_setfield(L, -2, "status"); + lua_setfield(L, -2, "audio"); +#endif + lua_newtable(L); // wada.mesh (read-only) lua_pushcfunction(L, meshContacts); lua_setfield(L, -2, "contacts"); + lua_pushcfunction(L, meshContactCount); lua_setfield(L, -2, "contact_count"); lua_pushcfunction(L, meshRxLog); lua_setfield(L, -2, "rx_log"); lua_pushcfunction(L, meshStats); lua_setfield(L, -2, "stats"); lua_pushcfunction(L, meshSelf); lua_setfield(L, -2, "self"); @@ -2397,6 +2628,9 @@ void hostTeardown() { Host* h = s_h; if (!h) return; s_h = nullptr; // bindings see "closed" from here on +#if CAP_LUA_AUDIO + luaHostAudioStop(h->generation, true); +#endif // A wada.ui.input dialog lives on lv_layer_top, so it would outlive the app // that opened it. Drop it before the state goes, not after. luaHostTextPromptDismiss(); @@ -2465,6 +2699,8 @@ bool luaAppLaunch(const char* id, const char* title, const char* src, size_t len Host* h = new Host(); h->heap.cap = kHeapCap; + h->generation = ++s_host_generation; + if (!h->generation) h->generation = ++s_host_generation; snprintf(h->id, sizeof h->id, "%s", id); snprintf(h->title, sizeof h->title, "%s", title ? title : id); diff --git a/src/ui-touch/ReaderContent.cpp b/src/ui-touch/ReaderContent.cpp new file mode 100644 index 0000000..2f1a385 --- /dev/null +++ b/src/ui-touch/ReaderContent.cpp @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ReaderContent.h" + +#include +#include + +namespace ReaderContent { +namespace { + +bool ciPrefix(const char* text, size_t text_len, const char* prefix) { + const size_t prefix_len = strlen(prefix); + if (text_len < prefix_len) return false; + for (size_t i = 0; i < prefix_len; ++i) { + char actual = text[i]; + char expected = prefix[i]; + if (actual >= 'A' && actual <= 'Z') actual += 32; + if (expected >= 'A' && expected <= 'Z') expected += 32; + if (actual != expected) return false; + } + return true; +} + +size_t decodeEntity(const char* text, size_t text_len, char* out, int* wrote) { + static const struct { const char* name; const char* utf8; } named[] = { + {"amp;","&"},{"lt;","<"},{"gt;",">"},{"quot;","\""},{"apos;","'"},{"nbsp;"," "}, + {"mdash;","\xe2\x80\x94"},{"ndash;","\xe2\x80\x93"},{"hellip;","\xe2\x80\xa6"}, + {"lsquo;","\xe2\x80\x98"},{"rsquo;","\xe2\x80\x99"},{"ldquo;","\xe2\x80\x9c"}, + {"rdquo;","\xe2\x80\x9d"},{"copy;","\xc2\xa9"},{"reg;","\xc2\xae"},{"euro;","\xe2\x82\xac"}, + {"deg;","\xc2\xb0"},{"middot;","\xc2\xb7"},{"bull;","\xe2\x80\xa2"},{"trade;","\xe2\x84\xa2"}, + }; + for (const auto& entity : named) { + const size_t name_len = strlen(entity.name); + if (text_len > name_len && strncmp(text + 1, entity.name, name_len) == 0) { + const int width = strlen(entity.utf8); + memcpy(out, entity.utf8, width); + *wrote = width; + return name_len + 1; + } + } + if (text_len > 3 && text[1] == '#') { + long codepoint = 0; + const bool hex = text[2] == 'x' || text[2] == 'X'; + size_t i = hex ? 3 : 2; + const size_t start = i; + for (; i < text_len && text[i] != ';'; ++i) { + const char c = text[i]; + int digit; + if (c >= '0' && c <= '9') digit = c - '0'; + else if (hex && c >= 'a' && c <= 'f') digit = c - 'a' + 10; + else if (hex && c >= 'A' && c <= 'F') digit = c - 'A' + 10; + else { i = start; break; } + codepoint = codepoint * (hex ? 16 : 10) + digit; + } + if (i > start && i < text_len && text[i] == ';' && codepoint > 0 && codepoint <= 0x10FFFF) { + int width = 0; + if (codepoint < 0x80) out[width++] = static_cast(codepoint); + else if (codepoint < 0x800) { + out[width++] = 0xC0 | (codepoint >> 6); + out[width++] = 0x80 | (codepoint & 0x3F); + } else if (codepoint < 0x10000) { + out[width++] = 0xE0 | (codepoint >> 12); + out[width++] = 0x80 | ((codepoint >> 6) & 0x3F); + out[width++] = 0x80 | (codepoint & 0x3F); + } else { + out[width++] = 0xF0 | (codepoint >> 18); + out[width++] = 0x80 | ((codepoint >> 12) & 0x3F); + out[width++] = 0x80 | ((codepoint >> 6) & 0x3F); + out[width++] = 0x80 | (codepoint & 0x3F); + } + *wrote = width; + return i + 1; + } + } + return 0; +} + +bool tagAttribute(const char* tag, size_t tag_len, const char* attribute, + char* out, size_t cap) { + const size_t attribute_len = strlen(attribute); + for (size_t i = 0; i + attribute_len + 1 < tag_len; ++i) { + if (!ciPrefix(tag + i, tag_len - i, attribute)) continue; + if (i > 0 && tag[i - 1] != ' ' && tag[i - 1] != '\t' && + tag[i - 1] != '\r' && tag[i - 1] != '\n') continue; + size_t cursor = i + attribute_len; + while (cursor < tag_len && (tag[cursor] == ' ' || tag[cursor] == '\t')) ++cursor; + if (cursor >= tag_len || tag[cursor] != '=') continue; + ++cursor; + while (cursor < tag_len && (tag[cursor] == ' ' || tag[cursor] == '\t')) ++cursor; + char quote = 0; + if (cursor < tag_len && (tag[cursor] == '"' || tag[cursor] == '\'')) { + quote = tag[cursor++]; + } + size_t written = 0; + while (cursor < tag_len && written + 1 < cap) { + const char c = tag[cursor]; + if (quote ? c == quote : (c == ' ' || c == '>' || c == '\t')) break; + out[written++] = c; + ++cursor; + } + out[written] = 0; + return written > 0; + } + return false; +} + +} // namespace + +bool resolveUrl(const char* base, const char* href, char* out, size_t cap) { + if (!base || !href || !out || cap == 0) return false; + while (*href == ' ') ++href; + const size_t href_len = strlen(href); + if (!href_len || href[0] == '#') return false; + if (ciPrefix(href, href_len, "javascript:") || ciPrefix(href, href_len, "mailto:") || + ciPrefix(href, href_len, "tel:") || ciPrefix(href, href_len, "data:")) return false; + + if (ciPrefix(href, href_len, "http://") || ciPrefix(href, href_len, "https://") || + ciPrefix(href, href_len, "sd:/")) { + snprintf(out, cap, "%s", href); + } else if (ciPrefix(base, strlen(base), "sd:/")) { + if (href[0] == '/' && href[1] == '/') { + snprintf(out, cap, "https:%s", href); + } else if (href[0] == '/') { + snprintf(out, cap, "sd:%s", href); + } else { + const char* last_slash = strrchr(base + 3, '/'); + if (last_slash) snprintf(out, cap, "%.*s%s", static_cast(last_slash - base + 1), base, href); + else snprintf(out, cap, "sd:/%s", href); + } + } else { + const char* scheme_end = strstr(base, "://"); + if (!scheme_end) return false; + const int scheme_len = static_cast(scheme_end - base); + const char* host = scheme_end + 3; + const char* host_end = strchr(host, '/'); + const int host_len = host_end ? static_cast(host_end - host) : static_cast(strlen(host)); + if (href[0] == '/' && href[1] == '/') { + snprintf(out, cap, "%.*s:%s", scheme_len, base, href); + } else if (href[0] == '/') { + snprintf(out, cap, "%.*s://%.*s%s", scheme_len, base, host_len, host, href); + } else { + const char* last_slash = strrchr(base, '/'); + if (last_slash && last_slash > scheme_end + 2) + snprintf(out, cap, "%.*s%s", static_cast(last_slash - base + 1), base, href); + else + snprintf(out, cap, "%.*s://%.*s/%s", scheme_len, base, host_len, host, href); + } + } + char* fragment = strchr(out, '#'); + if (fragment) *fragment = 0; + return out[0] != 0; +} + +size_t htmlToText(const char* html, size_t html_len, + char* out, size_t out_cap, const char* base, + Link* links, size_t link_capacity, size_t* link_count) { + if (link_count) *link_count = 0; + if (!html || !out || out_cap == 0) return 0; + + size_t out_len = 0; + size_t links_len = 0; + int pending_newlines = 0; + bool pending_space = false; + bool started = false; + bool in_anchor = false; + uint32_t anchor_start = 0; + char anchor_href[HREF_CAPACITY] = ""; + auto emit = [&](char c) { if (out_len + 1 < out_cap) out[out_len++] = c; }; + auto flush = [&]() { + if (!started) { + pending_newlines = 0; + pending_space = false; + return; + } + while (pending_newlines > 0) { emit('\n'); --pending_newlines; } + if (pending_space) { emit(' '); pending_space = false; } + }; + auto finishAnchor = [&]() { + if (in_anchor && out_len > anchor_start && links && links_len < link_capacity) { + links[links_len].start = anchor_start; + links[links_len].end = static_cast(out_len); + snprintf(links[links_len].href, sizeof links[links_len].href, "%s", anchor_href); + ++links_len; + } + in_anchor = false; + }; + static const char* blocks[] = { + "p","div","br","li","ul","ol","tr","h1","h2","h3","h4","h5","h6", + "section","article","header","footer","table","blockquote","pre","hr","nav","title","body", nullptr + }; + + size_t i = 0; + while (i < html_len && out_len + 5 < out_cap) { + const char c = html[i]; + if (c == '<') { + if (html_len - i >= 4 && html[i + 1] == '!' && html[i + 2] == '-' && html[i + 3] == '-') { + size_t end = i + 4; + while (end + 2 < html_len && !(html[end] == '-' && html[end + 1] == '-' && html[end + 2] == '>')) ++end; + i = end + 2 < html_len ? end + 3 : html_len; + continue; + } + const bool script = ciPrefix(html + i, html_len - i, "= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z') || + (value >= '0' && value <= '9')) { + name[name_len++] = value >= 'A' && value <= 'Z' ? value + 32 : value; + ++name_cursor; + } else { + break; + } + } + name[name_len] = 0; + size_t tag_end = i + 1; + while (tag_end < html_len && html[tag_end] != '>') ++tag_end; + if (name[0] == 'a' && name[1] == 0) { + finishAnchor(); + if (!closing) { + char raw_href[300]; + if (base && links && + tagAttribute(html + i, (tag_end < html_len ? tag_end : html_len) - i, + "href", raw_href, sizeof raw_href) && + resolveUrl(base, raw_href, anchor_href, sizeof anchor_href)) { + flush(); + in_anchor = true; + anchor_start = static_cast(out_len); + } + } + } + i = tag_end < html_len ? tag_end + 1 : html_len; + for (int block = 0; blocks[block]; ++block) { + if (strcmp(name, blocks[block]) == 0) { + pending_space = false; + if (pending_newlines < 2) ++pending_newlines; + break; + } + } + continue; + } + if (c == '&') { + char entity[8]; + int width = 0; + const size_t used = decodeEntity(html + i, html_len - i, entity, &width); + if (used) { + for (int byte = 0; byte < width; ++byte) { + if (entity[byte] == ' ') { + if (started) pending_space = true; + } else { + flush(); emit(entity[byte]); started = true; + } + } + i += used; + continue; + } + flush(); emit('&'); started = true; ++i; + continue; + } + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + if (started) pending_space = true; + ++i; + continue; + } + flush(); emit(c); started = true; ++i; + } + finishAnchor(); + out[out_len < out_cap ? out_len : out_cap - 1] = 0; + if (link_count) *link_count = links_len; + return out_len; +} + +} // namespace ReaderContent \ No newline at end of file diff --git a/src/ui-touch/ReaderContent.h b/src/ui-touch/ReaderContent.h new file mode 100644 index 0000000..2c442fc --- /dev/null +++ b/src/ui-touch/ReaderContent.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace ReaderContent { + +constexpr size_t HREF_CAPACITY = 240; + +struct Link { + uint32_t start; + uint32_t end; + char href[HREF_CAPACITY]; +}; + +bool resolveUrl(const char* base, const char* href, char* out, size_t cap); + +size_t htmlToText(const char* html, size_t html_len, + char* out, size_t out_cap, const char* base, + Link* links, size_t link_capacity, size_t* link_count); + +} // namespace ReaderContent \ No newline at end of file diff --git a/src/ui-touch/SnakeGame.cpp b/src/ui-touch/SnakeGame.cpp index f83b429..4ba731b 100644 --- a/src/ui-touch/SnakeGame.cpp +++ b/src/ui-touch/SnakeGame.cpp @@ -25,9 +25,12 @@ SnakeGame* SnakeGame::s_active = nullptr; bool SnakeGame::isOpen() { return s_active != nullptr && s_active->root_ != nullptr; } void SnakeGame::steer(int dx, int dy) { - if (!s_active || !s_active->started_ || s_active->over_) return; + if (!s_active || s_active->over_) return; // Trackball deltas -> one cardinal direction (dominant axis). if (dx == 0 && dy == 0) return; + // Keyboard-only boards (M9) have no touch "New game" tap target; first + // directional input should start immediately. + if (!s_active->started_) s_active->startGame(); const int adx = dx < 0 ? -dx : dx, ady = dy < 0 ? -dy : dy; if (adx >= ady) s_active->setDir(dx > 0 ? 1 : -1, 0); else s_active->setDir(0, dy > 0 ? 1 : -1); @@ -105,7 +108,7 @@ void SnakeGame::updateScoreLabel() { if (over_) lv_label_set_text_fmt(score_, TR("Game over \xe2\x80\x94 score %d (tap to restart)"), score_val_); else if (paused_) lv_label_set_text_fmt(score_, TR("Paused \xe2\x80\x94 score %d"), score_val_); else if (started_) lv_label_set_text_fmt(score_, TR("score %d"), score_val_); - else lv_label_set_text(score_, TR("Snake \xe2\x80\x94 swipe or roll the trackball")); + else lv_label_set_text(score_, TR("Snake \xe2\x80\x94 swipe, roll, or press a direction")); } void SnakeGame::step() { diff --git a/src/ui-touch/SnakeGame.h b/src/ui-touch/SnakeGame.h index 426ffe7..7bc63ed 100644 --- a/src/ui-touch/SnakeGame.h +++ b/src/ui-touch/SnakeGame.h @@ -6,9 +6,10 @@ // Self-contained Snake mini-game launched from the Apps drawer. // // Owns a full-screen overlay on lv_layer_top: a canvas playfield filling the -// unoccupied screen, a score line, and a close button. The game waits on a -// "New game" button (doesn't start moving on open). Steer by swipe OR trackball -// (UITask routes the trackball here via isOpen()/steer() and hides the cursor); +// unoccupied screen, a score line, and a close button. The game can start from +// the "New game" button OR from the first directional steer input. Steer by +// swipe/gesture, trackball, or hardware directional keys (UITask routes board +// input here via isOpen()/steer()); // tap to restart after a game over; the X closes it. One instance at a time. // // Decoupled from UITask internals — depends only on LVGL and lvglPsramAlloc. diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index dcdfcd9..9c08415 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -19,6 +19,20 @@ #include #include #include // chat-store write diagnostics surface errno (ENFILE vs ENOSPC vs EIO) +#if CAP_LUA_AUDIO +static void* wadaMp3Scratch(); +#define MINIMP3_ONLY_MP3 +#define MINIMP3_NO_SIMD +#define MINIMP3_SCRATCH ((mp3dec_scratch_t*)wadaMp3Scratch()) +#define MINIMP3_IMPLEMENTATION +#include "../../lib/minimp3/minimp3.h" +#undef MINIMP3_IMPLEMENTATION +#undef MINIMP3_SCRATCH +#undef MINIMP3_NO_SIMD +#undef MINIMP3_ONLY_MP3 +static void* s_wada_mp3_scratch = nullptr; +static void* wadaMp3Scratch() { return s_wada_mp3_scratch; } +#endif #if defined(ESP32) #include #include @@ -52,6 +66,7 @@ #include // detect/read/erase the panic coredump for the crash-report export #include #include // xTaskGetCurrentTaskHandleForCPU / pcTaskGetName — Task-WDT crash self-record + #include #else // Tanmatsu's 16M.csv has no coredump partition yet — stub so the crash-export compiles + links. #include @@ -111,6 +126,7 @@ #include "LuaAppHost.h" // sandboxed Lua apps (LUA_APPS.md Phase 1; self-gated on CAP_LUA_APPS) #include "AppPage.h" // shared full-screen app-page chrome (both of the above use it) +#include "ReaderContent.h" // host-tested HTML text extraction + local/network link resolution #if defined(HAS_TOUCH_UI) #include @@ -119,7 +135,7 @@ #include #endif #if defined(HAS_TDECK_KEYBOARD) - #include + #include "../helpers/input/TDeckKeyboard.h" #elif defined(HAS_M9_KEYBOARD) #include #endif @@ -130,7 +146,7 @@ #include // wada.sys.accel() source (luaHostAccel) #endif #if defined(HAS_PAGER_KEYBOARD) - #include + #include "../helpers/input/PagerKeyboard.h" #endif #if defined(HAS_PAGER_ENCODER) #include @@ -507,6 +523,18 @@ extern "C" const lv_font_t cc_icons_16; extern "C" const lv_font_t extras_12; extern "C" const lv_font_t extras_14; extern "C" const lv_font_t extras_16; +// extras_lat_28 used to be compiled HAS_TANMATSU-only (Large/Huge UI-scale Latin- +// accent fallback, issue #129); it's now compiled on every board (extras_lat_28.c) +// for the "at a glance" notification's message body (atGlanceEnsureFont()) -- +// montserrat_28 was picked over 24 because only 12/14/16/28 are actually built +// into this project's vendored LVGL config (LV_FONT_MONTSERRAT_24 isn't enabled). +extern "C" const lv_font_t extras_lat_28; +// extras_lat_20 was HAS_TANMATSU-only too (Large/Huge UI-scale fallback); the +// T-Deck now also builds it, for its 20px at-glance body experiment (see +// atGlanceEnsureFont()) -- extras_lat_20.c's own gate was widened to match. +#if defined(HAS_TANMATSU) || defined(HAS_TDECK_GT911) +extern "C" const lv_font_t extras_lat_20; +#endif #if defined(TLORA_PAGER) // Full multilingual fallbacks at the Pager's accessible text sizes. These keep // accented Latin, Greek, Cyrillic, Arabic, punctuation, and units aligned with @@ -519,9 +547,7 @@ extern "C" const lv_font_t extras_24; // neighbours at Large/Huge UI scale (issue #129). Room for these was made by // storing the extras fonts compressed (LV_USE_FONT_COMPRESSED) — nothing lost, // net binary SMALLER, so the P4 app-load ceiling is respected. -extern "C" const lv_font_t extras_lat_20; extern "C" const lv_font_t extras_lat_24; -extern "C" const lv_font_t extras_lat_28; #endif static lv_font_t g_font_12; static lv_font_t g_font_14; @@ -832,6 +858,56 @@ static inline void tileFetchPendingDec() { // below are shared; T-Deck and the pager each get their own I2S install/ // tone/WAV functions, since the pager additionally drives an ES8311 codec // over I2C that the T-Deck's plain MAX98357A DAC doesn't have. +#if CAP_AUDIO_STREAM +static uint32_t wavRd32(File& f){ uint8_t b[4]; if(f.read(b,4)!=4) return 0; return (uint32_t)b[0]|((uint32_t)b[1]<<8)|((uint32_t)b[2]<<16)|((uint32_t)b[3]<<24); } +static uint16_t wavRd16(File& f){ uint8_t b[2]; if(f.read(b,2)!=2) return 0; return (uint16_t)(b[0]|(b[1]<<8)); } +static bool wavParse(File& f, uint16_t* pch, uint32_t* prate, uint32_t* pdata) { + char tag[4]; + if (f.read((uint8_t*)tag,4)!=4 || memcmp(tag,"RIFF",4)) return false; + const uint32_t riff_size = wavRd32(f); + const uint64_t riff_end = 8u + (uint64_t)riff_size; + if (riff_size < 4 || riff_end > f.size() || riff_end > 0xFFFFFFFFu) return false; + if (f.read((uint8_t*)tag,4)!=4 || memcmp(tag,"WAVE",4)) return false; + uint16_t fmt=0, ch=0, bits=0; uint32_t rate=0, dlen=0; bool hf=false, hd=false; + while ((uint64_t)f.position() + 8u <= riff_end) { + if (f.read((uint8_t*)tag,4)!=4) break; + uint32_t csz = wavRd32(f); + if (!memcmp(tag,"fmt ",4)) { + if (csz < 16) return false; + fmt = wavRd16(f); ch = wavRd16(f); rate = wavRd32(f); wavRd32(f); wavRd16(f); bits = wavRd16(f); + const uint64_t skip = (uint64_t)(csz - 16) + (csz & 1u); + const uint64_t next = (uint64_t)f.position() + skip; + if (next > riff_end || (skip && !f.seek((uint32_t)next))) return false; + hf = true; + } else if (!memcmp(tag,"data",4)) { + if ((uint64_t)f.position() + csz > riff_end) return false; + dlen = csz; hd = true; break; + } else { + const uint64_t skip = (uint64_t)csz + (csz & 1u); + const uint64_t next = (uint64_t)f.position() + skip; + if (next > riff_end || (skip && !f.seek((uint32_t)next))) return false; + } + } + if (!hf || !hd || fmt != 1 || bits != 16 || (ch != 1 && ch != 2) || rate < 8000 || rate > 48000) + return false; + if (pch) *pch = ch; if (prate) *prate = rate; if (pdata) *pdata = dlen; + return true; +} +#endif + +#if CAP_LUA_AUDIO +static volatile bool s_lua_audio_active = false; +static volatile uint32_t s_lua_audio_storage_pending = 0; +static volatile bool s_lua_audio_storage_active = false; +static inline bool luaAudioActive() { + return __atomic_load_n(&s_lua_audio_active, __ATOMIC_ACQUIRE); +} +static inline bool luaAudioStorageBusy() { + return __atomic_load_n(&s_lua_audio_storage_pending, __ATOMIC_ACQUIRE) != 0 || + __atomic_load_n(&s_lua_audio_storage_active, __ATOMIC_ACQUIRE); +} +#endif + #if defined(HELTEC_LORA_V4_R8) || defined(HAS_THINKNODE_M9) static bool fmSdTryMount(); // V4-R8/M9 microSD — fwd decl (defined in the mount-helper block below; sdRestoreRun needs it) #endif @@ -988,29 +1064,6 @@ static void pagerPlayToneRaw(int freq, int ms) { // amp. Supports PCM 16-bit, mono or stereo (downmixed to the mono amp), sample // rate read from the header; capped to ~6 s so a stray big file can't hold the // notify task. Any problem -> false, and the caller falls back to the chime. -static uint32_t wavRd32(File& f){ uint8_t b[4]; if(f.read(b,4)!=4) return 0; return (uint32_t)b[0]|((uint32_t)b[1]<<8)|((uint32_t)b[2]<<16)|((uint32_t)b[3]<<24); } -static uint16_t wavRd16(File& f){ uint8_t b[2]; if(f.read(b,2)!=2) return 0; return (uint16_t)(b[0]|(b[1]<<8)); } -static bool wavParse(File& f, uint16_t* pch, uint32_t* prate, uint32_t* pdata) { - char tag[4]; - if (f.read((uint8_t*)tag,4)!=4 || memcmp(tag,"RIFF",4)) return false; - wavRd32(f); - if (f.read((uint8_t*)tag,4)!=4 || memcmp(tag,"WAVE",4)) return false; - uint16_t fmt=0, ch=0, bits=0; uint32_t rate=0, dlen=0; bool hf=false, hd=false; - while (f.available() >= 8) { - if (f.read((uint8_t*)tag,4)!=4) break; - uint32_t csz = wavRd32(f); - if (!memcmp(tag,"fmt ",4)) { - fmt = wavRd16(f); ch = wavRd16(f); rate = wavRd32(f); wavRd32(f); wavRd16(f); bits = wavRd16(f); - if (csz > 16) f.seek(f.position() + (csz - 16)); - hf = true; - } else if (!memcmp(tag,"data",4)) { dlen = csz; hd = true; break; } - else f.seek(f.position() + csz + (csz & 1)); - } - if (!hf || !hd || fmt != 1 || bits != 16 || (ch != 1 && ch != 2) || rate < 8000 || rate > 48000) - return false; - if (pch) *pch = ch; if (prate) *prate = rate; if (pdata) *pdata = dlen; - return true; -} static bool wavOpen(const char* prefpath, File& f) { if (!prefpath || !prefpath[0]) return false; fs::FS* fsp = &SPIFFS; const char* fp = prefpath; @@ -1302,6 +1355,9 @@ static void tanBeep(); // I2S notification tick; defined far below (with the C #endif // Play the platform's notification chime. Caller checks the buzzer/sound pref. static inline void uiPlaySlot(int slot) { +#if CAP_LUA_AUDIO + if (luaAudioActive()) return; +#endif #if defined(HAS_TDECK_GT911) tdeckPlayNotifySlot(slot); #elif defined(HAS_TDISPLAY_P4) @@ -1321,6 +1377,9 @@ static inline void uiPlayMention() { uiPlaySlot(TOUCH_SND_MEN); } // Preview an arbitrary WAV file (not yet saved to a slot) -- used by the // sound picker's "play" button before the user commits to a choice. static inline void uiPreviewWavFile(const char* path) { +#if CAP_LUA_AUDIO + if (luaAudioActive()) return; +#endif #if defined(HAS_TDECK_GT911) tdeckPreviewWavFile(path); #elif defined(TLORA_PAGER) @@ -2307,6 +2366,8 @@ struct SettingsModalState { lv_obj_t* exp_boost_sw; lv_obj_t* exp_dc_sw; lv_obj_t* wifi_sw; + lv_obj_t* glance_en_sw; + lv_obj_t* glance_locked_sw; lv_obj_t* wifi_ssid_ta; lv_obj_t* wifi_pwd_ta; /** Transports modal: live STA line (IP when connected, else Arduino WiFi status). */ @@ -2723,6 +2784,7 @@ static lv_obj_t* s_cc_sys_label = nullptr; // CPU/RAM/PSRAM/IP line; refreshed static void ccBuildSysInfo(char* buf, size_t n); // fwd-decl; defined with the CC helpers below static void closeControlCenter(); // defined in the control-center section below static void openControlCenter(); // defined in the control-center section below (early decl for the M9 CTRL key) +static void takeScreenshotToSd(); // shared by the status-bar hold and M9 Control Center action static lv_obj_t* s_power_menu = nullptr; // power off / reboot menu (control center) static void closePowerMenu(); // defined in the control-center section below static void openPowerMenu(); // hold the red ✕ (F1) on the Tanmatsu → power off / reboot @@ -5362,7 +5424,7 @@ static void versionCheckUpdateUi() { snprintf(b, sizeof b, TR("Firmware beta_%d\nCouldn't reach the update server"), my_n); lv_obj_set_style_text_color(s_update_about_lbl, lv_color_hex(COLOR_SUB), LV_PART_MAIN); } else { - snprintf(b, sizeof b, "Firmware beta_%d\nChecking for updates over Wi-Fi…", my_n); + snprintf(b, sizeof b, TR("Firmware beta_%d\nChecking for updates over Wi-Fi…"), my_n); lv_obj_set_style_text_color(s_update_about_lbl, lv_color_hex(COLOR_SUB), LV_PART_MAIN); } lv_label_set_text(s_update_about_lbl, b); @@ -5579,7 +5641,7 @@ static void otaPrevPickCb(lv_event_t* e) { s_ota_pending_n = v; otaPrevModalClose(); char m[96]; - snprintf(m, sizeof m, "Install beta_%d?\nThis downgrades the firmware and reboots.", v); + snprintf(m, sizeof m, TR("Install beta_%d?\nThis downgrades the firmware and reboots."), v); showConfirm(m, TR("Install"), otaConfirmInstallApply); } @@ -7015,7 +7077,12 @@ static void keyboardCb(lv_event_t* e) { } static void composerFocusCb(lv_event_t* e) { - if (lv_event_get_code(e) != LV_EVENT_FOCUSED) return; + const lv_event_code_t code = lv_event_get_code(e); + if (code != LV_EVENT_FOCUSED && code != LV_EVENT_CLICKED) return; + if (code == LV_EVENT_CLICKED) { + lv_indev_t* act = lv_indev_get_act(); + if (act && (lv_indev_get_scroll_obj(act) || lv_indev_get_scroll_dir(act) != LV_DIR_NONE)) return; + } auto* p = static_cast(lv_event_get_user_data(e)); if (p && p->detail_open) { showKb(p); noteKbActivity(); } } @@ -8717,6 +8784,29 @@ static void settingsFieldDefocusCb(lv_event_t* /*e*/) { accentBoxHide(); } // The full text-edit gestures — defined further down with the edit-menu code. static void composerEditClickedCb(lv_event_t* e); static void composerEditLongPressCb(lv_event_t* e); +// A widget being destroyed still gets one last LV_EVENT_DEFOCUSED, and that is a +// trap for every save-on-blur handler in this file. +// +// obj_del_core sends LV_EVENT_DELETE, then runs the destructor, and the +// destructor's lv_group_remove_obj refocuses the group -- which delivers +// DEFOCUSED to the object that is already half torn down. A blur handler then +// reads the textarea's text and writes it back, on memory the destructor is in +// the middle of freeing. +// +// That is the beta_69 crash in #330: open Radio & mesh, go to Known regions, add +// and remove a few, leave the page. The regions overlay is deleted +// asynchronously, its teardown refocuses, and the poll-interval field's blur +// save runs inside its own destructor. Decoded from the reporter's core dump. +// +// DELETE always arrives before the destructor, so recording the object here and +// checking it in the blur handlers closes the window. The pointer is only ever +// compared, never dereferenced. +static lv_obj_t* s_ta_deleting = nullptr; +static void settingsFieldDeleteCb(lv_event_t* e) { s_ta_deleting = lv_event_get_target(e); } +static inline bool blurFromDelete(lv_event_t* e) { + return lv_event_get_target(e) == s_ta_deleting; +} + static void attachSettingsTaEvents(lv_obj_t* ta) { lv_obj_add_event_cb(ta, settingsFieldFocusCb, LV_EVENT_FOCUSED, nullptr); lv_obj_add_event_cb(ta, settingsFieldFocusCb, LV_EVENT_CLICKED, nullptr); @@ -8731,6 +8821,9 @@ static void attachSettingsTaEvents(lv_obj_t* ta) { lv_obj_add_event_cb(ta, composerEditClickedCb, LV_EVENT_CLICKED, nullptr); lv_obj_add_event_cb(ta, composerEditLongPressCb, LV_EVENT_LONG_PRESSED, nullptr); lv_obj_add_event_cb(ta, settingsFieldDefocusCb, LV_EVENT_DEFOCUSED, nullptr); + // Must be on every settings field, not just the ones that save on blur: this is + // what tells those handlers the coming DEFOCUSED is a destructor, not a user. + lv_obj_add_event_cb(ta, settingsFieldDeleteCb, LV_EVENT_DELETE, nullptr); } // ===== Text selection + edit menu ========================================== @@ -9177,6 +9270,7 @@ static lv_obj_t* createSettingsModal(const char* title, SettingsModalKind kind) } static void saveProfileNameCb(lv_event_t* e) { + if (blurFromDelete(e)) return; // the widget is being destroyed const lv_event_code_t _c = lv_event_get_code(e); if ((_c != LV_EVENT_CLICKED && _c != LV_EVENT_DEFOCUSED) || !g_lv.task || !g_set_modal.name_ta) return; kbMirrorSyncToReal(); @@ -9190,6 +9284,7 @@ static void saveProfileNameCb(lv_event_t* e) { } static void saveProfilePosCb(lv_event_t* e) { + if (blurFromDelete(e)) return; // the widget is being destroyed const lv_event_code_t _c = lv_event_get_code(e); if ((_c != LV_EVENT_CLICKED && _c != LV_EVENT_DEFOCUSED) || !g_lv.task) return; const bool silent = (_c == LV_EVENT_DEFOCUSED); // blur auto-save: quiet if mid-edit @@ -9206,6 +9301,7 @@ static void saveProfilePosCb(lv_event_t* e) { } static void saveRadioParamsCb(lv_event_t* e) { + if (blurFromDelete(e)) return; // the widget is being destroyed const lv_event_code_t _c = lv_event_get_code(e); if ((_c != LV_EVENT_CLICKED && _c != LV_EVENT_DEFOCUSED) || !g_lv.task) return; const bool silent = (_c == LV_EVENT_DEFOCUSED); // blur auto-save: apply quietly, no alerts @@ -9282,6 +9378,7 @@ static void saveRadioParamsCb(lv_event_t* e) { } static void saveAutoAddCb(lv_event_t* e) { + if (blurFromDelete(e)) return; // the widget is being destroyed const lv_event_code_t _c = lv_event_get_code(e); if ((_c != LV_EVENT_CLICKED && _c != LV_EVENT_DEFOCUSED) || !g_lv.task) return; const bool silent = (_c == LV_EVENT_DEFOCUSED); // blur auto-save: quiet if mid-edit @@ -10452,6 +10549,7 @@ static void radioRetryEchoToggleCb(lv_event_t* e) { the_mesh.setCompanionRetryEnabled(on); } static void radioSigPollSaveCb(lv_event_t* e) { + if (blurFromDelete(e)) return; // the widget is being destroyed const lv_event_code_t _c = lv_event_get_code(e); if ((_c != LV_EVENT_CLICKED && _c != LV_EVENT_DEFOCUSED) || !s_radio_sig_poll_ta) return; kbMirrorSyncToReal(); @@ -11106,6 +11204,7 @@ static void buildQuickReplySettings() { } static void saveScreenTimeoutCb(lv_event_t* e) { + if (blurFromDelete(e)) return; // the widget is being destroyed const lv_event_code_t _c = lv_event_get_code(e); if ((_c != LV_EVENT_CLICKED && _c != LV_EVENT_DEFOCUSED) || !g_lv.task) return; if (!g_set_modal.screen_to_ta) return; @@ -11802,6 +11901,38 @@ static void lockOnScreenOffToggleCb(lv_event_t* e) { if (g_lv.task) g_lv.task->showAlert(unlock_hint, 1600); } +// "At a glance" is read straight from NVS at message time (not cached), same as +// the sound/DND checks right next to its call site in newMsgImpl -- it's not a +// per-loop hot path, just once per inbound message. +static void glanceWhenLockedToggleCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + const bool on = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED); +#if defined(ESP32) + touchPrefsSetGlanceWhenLocked(on); +#endif + if (g_lv.task) { + g_lv.task->showAlert(on ? TR("Glance shows message previews\nwhile the screen is locked") + : TR("Glance only shows when unlocked"), 1600); + } +} + +// Master "At a glance" toggle (Options > Display): gates the whole feature. +// The secondary "while locked" switch's own saved preference is left +// alone -- only its editability follows the master, so re-enabling always +// restores its true prior state. +static void glanceEnabledToggleCb(lv_event_t* e) { + if (lv_event_get_code(e) != LV_EVENT_VALUE_CHANGED) return; + const bool on = lv_obj_has_state(lv_event_get_target(e), LV_STATE_CHECKED); +#if defined(ESP32) + touchPrefsSetGlanceEnabled(on); +#endif + if (g_set_modal.glance_locked_sw) { + if (on) lv_obj_clear_state(g_set_modal.glance_locked_sw, LV_STATE_DISABLED); + else lv_obj_add_state(g_set_modal.glance_locked_sw, LV_STATE_DISABLED); + } + if (g_lv.task) g_lv.task->showAlert(on ? TR("At a glance enabled") : TR("At a glance disabled"), 1200); +} + #if CAP_TRACKBALL // Invert the scrollball direction everywhere it drives motion (cursor, map pan, // emoji selector). Cached in s_tb_reverse so the per-tick poll never hits NVS. @@ -12891,6 +13022,41 @@ static void buildDeviceSettings(int sec) { y += LV_MAX(40, h + 12); } + /* At a glance: master enable for the "at a glance" message-preview overlay + (see atGlanceShow()/newMsgImpl()) -- on by default, matching the + previously-unconditional behavior. Gates the "while locked" switch below. */ + bool glance_on = true; + { + int h = settingsRowLabel(body, y, 6, TR("At a glance"), COLOR_SUB, nullptr, 56); + g_set_modal.glance_en_sw = lv_switch_create(body); + lv_obj_align(g_set_modal.glance_en_sw, LV_ALIGN_TOP_RIGHT, 0, y); +#if defined(ESP32) + glance_on = touchPrefsGetGlanceEnabled(); + if (glance_on) lv_obj_add_state(g_set_modal.glance_en_sw, LV_STATE_CHECKED); +#endif + lv_obj_add_event_cb(g_set_modal.glance_en_sw, glanceEnabledToggleCb, LV_EVENT_VALUE_CHANGED, nullptr); + y += LV_MAX(40, h + 12); + } + +#if !defined(HAS_TDISPLAY_P4) + /* "At a glance" also fires while manually/idle locked, not just while + unlocked+dimmed -- off by default since it's a real trade-off (message + text becomes readable off a locked device). Disabled (not cleared) while + the master switch above is off. Moved here from Options > Lock screen so + it sits with the feature it modifies. */ + { + int h = settingsRowLabel(body, y, 6, TR("At a glance while locked"), COLOR_SUB, nullptr, 56); + g_set_modal.glance_locked_sw = lv_switch_create(body); + lv_obj_align(g_set_modal.glance_locked_sw, LV_ALIGN_TOP_RIGHT, 0, y); +#if defined(ESP32) + if (touchPrefsGetGlanceWhenLocked()) lv_obj_add_state(g_set_modal.glance_locked_sw, LV_STATE_CHECKED); + if (!glance_on) lv_obj_add_state(g_set_modal.glance_locked_sw, LV_STATE_DISABLED); +#endif + lv_obj_add_event_cb(g_set_modal.glance_locked_sw, glanceWhenLockedToggleCb, LV_EVENT_VALUE_CHANGED, nullptr); + y += LV_MAX(40, h + 12); + } +#endif + #if defined(HAS_EXPANSION_KIT) /* Show Sensors tab (V4 Expansion Kit): toggles the bottom Sensors tab + the Home env widget. Also auto-hidden when no environment sensor is attached. @@ -13916,6 +14082,7 @@ static void bleEnableSwitchCb(lv_event_t* e) { // Pairing code: persist a 6-digit PIN on blur (applies next reboot, same contract as the // companion CMD_SET_DEVICE_PIN). Silent if the field is mid-edit (not exactly 6 digits). static void blePinSaveCb(lv_event_t* e) { + if (blurFromDelete(e)) return; // the widget is being destroyed if (lv_event_get_code(e) != LV_EVENT_DEFOCUSED || !g_lv.task) return; if (!s_ble_pin_ta || !lv_obj_is_valid(s_ble_pin_ta)) return; kbMirrorSyncToReal(); @@ -14912,14 +15079,21 @@ static void wifiRebuildNetworkList() { // Manual rescan (the scan is also kicked on open). Label flips while one runs. s_wifi_list_y += SC(6); - wifiListRow(scanning_now ? LV_SYMBOL_REFRESH " Scanning\xE2\x80\xA6" - : LV_SYMBOL_REFRESH " Scan again", + // The glyph has to be joined at runtime: TR() returns a pointer, so it cannot + // take part in the compiler's adjacent-string-literal concatenation. + char wifi_rescan[48]; + snprintf(wifi_rescan, sizeof wifi_rescan, LV_SYMBOL_REFRESH " %s", + scanning_now ? TR("Scanning\xE2\x80\xA6") : TR("Scan again")); + wifiListRow(wifi_rescan, nullptr, lv_color_hex(scanning_now ? COLOR_SUB : COLOR_TEXT), scanning_now ? nullptr : wifiRefreshRowCb, nullptr); // Hidden / manual network. s_wifi_list_y += SC(3); - wifiListRow(LV_SYMBOL_PLUS " Other (hidden) network\xE2\x80\xA6", nullptr, lv_color_hex(COLOR_ACCENT), wifiHiddenRowCb, nullptr); + char wifi_hidden[64]; + snprintf(wifi_hidden, sizeof wifi_hidden, LV_SYMBOL_PLUS " %s", + TR("Other (hidden) network\xE2\x80\xA6")); + wifiListRow(wifi_hidden, nullptr, lv_color_hex(COLOR_ACCENT), wifiHiddenRowCb, nullptr); lv_obj_set_height(s_wifi_list_cont, s_wifi_list_y + SC(2)); // grow the card to fit navMarkDirty(); } @@ -15479,16 +15653,10 @@ static void actionSheetPingCb(lv_event_t* e) { bool ok = the_mesh.getContactByIdx(s_action_sheet_mesh_idx, c); closeActionSheet(); if (!ok) { g_lv.task->showAlert(TR("Contact gone"), 1200); return; } - /* sendStatusPingWithGuestLoginForUI() pipelines a blank-password LOGIN - * before the STATUS REQ. Repeaters refuse to decrypt a PAYLOAD_TYPE_REQ - * from a sender that isn't already in their ACL, and the ACL is only - * populated by a successful sendLogin. A blank-password login matches - * repeaters with guest_password = "" (typical default) and adds us as a - * guest; subsequent REQs from this device then decrypt cleanly. The - * follow-up STATUS REQ also registers _ui_pending_status so the reply - * routes back via UITask::onPingReply (not eaten by the companion-serial - * pending_status branch). */ - int r = the_mesh.sendStatusPingWithGuestLoginForUI(c); + /* Wait for the guest LOGIN response before sending STATUS. Besides avoiding + * the first-contact ACL race, the interactive login re-discovers the path so + * older repeaters can return LOGIN_OK before the request is sent. */ + int r = the_mesh.uiSendRequestAfterGuestLogin(c, MyMesh::UiReqKind::Status); if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) { copyUtf8ReplacingMissingGlyphs(&g_font_14, s_ui_ping_target_name, sizeof(s_ui_ping_target_name), @@ -15553,9 +15721,8 @@ static void actionSheetTelemetryCb(lv_event_t* e) { openTelemetryWindow(s_telem_node, s_telem_name, TELEM_HISTORY); #else // No telemetry window on this board — send straight away and toast the result. - // Chain a guest LOGIN ahead of the REQ (repeaters/sensors need us in their ACL - // before they decrypt a PAYLOAD_TYPE_REQ; see sendStatusPingWithGuestLoginForUI). - int r = the_mesh.sendTelemetryRequestWithGuestLoginForUI(c); + // Wait for guest LOGIN + path discovery before sending the telemetry REQ. + int r = the_mesh.uiSendRequestAfterGuestLogin(c, MyMesh::UiReqKind::Telemetry); if (r == MSG_SEND_SENT_FLOOD || r == MSG_SEND_SENT_DIRECT) g_lv.task->showAlert(TR("Telemetry req\xe2\x80\xa6"), 1400); else @@ -20033,8 +20200,10 @@ static void fmOpenStorage(fs::FS* fs, const char* store, const char* path) { } static void fmInternalClickCb(lv_event_t* e) { if (lv_event_get_code(e) != LV_EVENT_CLICKED) return; -#if defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) - fmOpenStorage(&FFat, "Internal", "/"); // the internal FAT data partition (locfd / 'storage') +#if defined(HAS_TANMATSU) + fmOpenStorage(&FFat, "Internal", "/"); // internal FAT data partition (locfd) +#elif defined(HAS_TDISPLAY_P4) + fmOpenStorage(&LittleFS, "Internal", "/"); // internal LittleFS 'storage' partition #else fmOpenStorage(&SPIFFS, "Internal", "/"); #endif @@ -21552,6 +21721,9 @@ static uint64_t s_tan_sd_size = 0; static uint32_t s_tan_sd_retry_after = 0; // backoff so an absent/cold card isn't re-probed every render static bool tanSdTryMount() { if (s_tan_sd_mounted) return true; +#if CAP_LUA_AUDIO + if (luaAudioStorageBusy()) return false; +#endif if (millis() < s_tan_sd_retry_after) return false; #if defined(HAS_TDISPLAY_P4) // Hot-insert path (no-op when main.cpp already mounted at boot): 20 MHz like the boot ladder, @@ -22015,10 +22187,13 @@ static void openSignalInfoPopup() { sig_row2_y = sig_row1_y + sig_line_h + 2; #endif - sigCell(stale && heard ? "Signal (stale)" : "Signal", 0, sig_hdr_y, COLOR_TEXT); + // sigCell does not translate its argument, so these wrap at the call site. The + // home-graph signal popup has its own TR()'d copy of the same three strings; + // this one was missed, so the card read English in every language (pisti87). + sigCell(stale && heard ? TR("Signal (stale)") : TR("Signal"), 0, sig_hdr_y, COLOR_TEXT); int cy; if (!heard) { - sigCell("nothing heard yet", 8, sig_row1_y, COLOR_SUB); + sigCell(TR("nothing heard yet"), 8, sig_row1_y, COLOR_SUB); #if defined(TLORA_PAGER) cy = sig_row1_y + lv_font_get_line_height(&g_font_12) + 10; #else @@ -22162,6 +22337,7 @@ static void openSignalInfoPopup() { // runs on a core-0 worker task so LVGL never blocks; the UI loop polls s_reader_dirty. #if defined(ESP32) && defined(MULTI_TRANSPORT_COMPANION) static const char kReaderDefaultUrl[] = "wadamesh.com"; +static const char kReaderHomeUrl[] = "sd:/home.htm"; static lv_obj_t* s_reader_root = nullptr; static lv_obj_t* s_reader_url_ta = nullptr; static lv_obj_t* s_reader_go = nullptr; @@ -22173,13 +22349,19 @@ static char* s_reader_text = nullptr; // PSRAM: extracted text (NUL static volatile bool s_reader_dirty = false; // worker -> UI: text/status changed static volatile bool s_reader_busy = false; // a fetch is in flight static volatile bool s_reader_ok = false; // last fetch produced a page (worker -> UI) +static volatile bool s_reader_home_missing = false; // initial SD probe failed; restore URL-entry state +static volatile bool s_reader_sd_busy = false; // open reader File owns the removable-card VFS +static TaskHandle_t s_reader_sd_owner = nullptr; +static volatile uint32_t s_reader_generation = 0; +static volatile uint32_t s_reader_task_generation = 0; +static char s_reader_pending_url[300] = ""; static char s_reader_msg[110] = ""; // status text (worker writes, UI shows) static TaskHandle_t s_reader_task = nullptr; static bool s_reader_pristine = false; // URL field still holds the untouched default static const size_t kReaderRawCap = 192 * 1024; // max raw HTML we buffer (PSRAM) static const size_t kReaderTextCap = 60 * 1024; // max extracted text we keep // Links found on the page: byte range in s_reader_text + resolved absolute href. -struct ReaderLink { uint32_t start, end; char href[240]; }; +using ReaderLink = ReaderContent::Link; static const int kReaderMaxLinks = 280; static ReaderLink* s_reader_links = nullptr; // PSRAM array, lazily allocated static int s_reader_nlinks = 0; @@ -22194,7 +22376,7 @@ static lv_obj_t* s_reader_back = nullptr; static lv_obj_t* s_reader_fwd = nullptr; static void closeReaderPage(); -static void readerNavigate(const char* in, bool push); +static bool readerNavigate(const char* in, bool push); static void readerUpdateNavButtons(); static void readerRenderBody(); static void readerSetAddrExpanded(bool exp); @@ -22207,135 +22389,107 @@ static bool readerCiPrefix(const char* s, size_t n, const char* pfx) { if (a >= 'A' && a <= 'Z') a += 32; if (b >= 'A' && b <= 'Z') b += 32; if (a != b) return false; } return true; } -// Decode one HTML entity at s[0]=='&'. Writes UTF-8 to out (>=4 bytes), sets *wrote, -// returns consumed input length; 0 if unrecognised (caller emits '&' literally). -static size_t readerEntity(const char* s, size_t n, char* out, int* wrote) { - static const struct { const char* name; const char* utf8; } named[] = { - {"amp;","&"},{"lt;","<"},{"gt;",">"},{"quot;","\""},{"apos;","'"},{"nbsp;"," "}, - {"mdash;","\xe2\x80\x94"},{"ndash;","\xe2\x80\x93"},{"hellip;","\xe2\x80\xa6"}, - {"lsquo;","\xe2\x80\x98"},{"rsquo;","\xe2\x80\x99"},{"ldquo;","\xe2\x80\x9c"}, - {"rdquo;","\xe2\x80\x9d"},{"copy;","\xc2\xa9"},{"reg;","\xc2\xae"},{"euro;","\xe2\x82\xac"}, - {"deg;","\xc2\xb0"},{"middot;","\xc2\xb7"},{"bull;","\xe2\x80\xa2"},{"trade;","\xe2\x84\xa2"}, +// A bounded Stream lets HTTPClient remove chunk framing while writing directly into +// PSRAM. Short-writing at the cap stops oversized pages without allocating a String. +class ReaderBufferStream final : public Stream { + public: + ReaderBufferStream(uint8_t* data, size_t capacity, uint32_t generation) + : data_(data), capacity_(capacity), generation_(generation) {} + using Print::write; + size_t write(uint8_t byte) override { return write(&byte, 1); } + size_t write(const uint8_t* data, size_t size) override { + const size_t before = size_; + const size_t room = size_ < capacity_ ? capacity_ - size_ : 0; + const size_t copied = size < room ? size : room; + if (copied) { + memcpy(data_ + size_, data, copied); + size_ += copied; + if (generation_ == s_reader_generation && (before >> 14) != (size_ >> 14)) { + snprintf(s_reader_msg, sizeof s_reader_msg, "Loading\xe2\x80\xa6 %uk", (unsigned)(size_ / 1024)); + s_reader_dirty = true; + } + } + return copied; + } + int available() override { return 0; } + int read() override { return -1; } + int peek() override { return -1; } + void flush() override {} + size_t size() const { return size_; } + bool full() const { return size_ == capacity_; } + + private: + uint8_t* data_; + size_t capacity_; + uint32_t generation_; + size_t size_ = 0; +}; + +enum class ReaderLocalResult : uint8_t { Ok, NoStorage, NotFound, ReadFailed }; + +static ReaderLocalResult readerReadLocal(const char* url, uint8_t* raw, size_t capacity, + size_t* total, uint32_t generation) { + fs::FS* storage = nullptr; + bool storage_claimed = false; + auto releaseStorage = [&]() { + if (!storage_claimed) return; + s_reader_sd_owner = nullptr; + s_reader_sd_busy = false; + storage_claimed = false; }; - for (auto& e : named) { size_t ln = strlen(e.name); - if (n > ln && strncmp(s + 1, e.name, ln) == 0) { int w = strlen(e.utf8); memcpy(out, e.utf8, w); *wrote = w; return ln + 1; } } - if (n > 3 && s[1] == '#') { // &#NNN; or &#xHH; - long cp = 0; bool hex = (s[2] == 'x' || s[2] == 'X'); size_t i = hex ? 3 : 2, start = i; - for (; i < n && s[i] != ';'; i++) { char c = s[i]; int d; - if (c >= '0' && c <= '9') d = c - '0'; - else if (hex && c >= 'a' && c <= 'f') d = c - 'a' + 10; - else if (hex && c >= 'A' && c <= 'F') d = c - 'A' + 10; else { i = start; break; } - cp = cp * (hex ? 16 : 10) + d; } - if (i > start && i < n && s[i] == ';' && cp > 0 && cp <= 0x10FFFF) { - int w = 0; - if (cp < 0x80) out[w++] = (char)cp; - else if (cp < 0x800) { out[w++] = 0xC0 | (cp >> 6); out[w++] = 0x80 | (cp & 0x3F); } - else if (cp < 0x10000) { out[w++] = 0xE0 | (cp >> 12); out[w++] = 0x80 | ((cp >> 6) & 0x3F); out[w++] = 0x80 | (cp & 0x3F); } - else { out[w++] = 0xF0 | (cp >> 18); out[w++] = 0x80 | ((cp >> 12) & 0x3F); out[w++] = 0x80 | ((cp >> 6) & 0x3F); out[w++] = 0x80 | (cp & 0x3F); } - *wrote = w; return i + 1; - } +#if defined(HAS_TDECK_GT911) || defined(TLORA_PAGER) || defined(HAS_THINKNODE_M9) || defined(HELTEC_LORA_V4_R8) + s_reader_sd_busy = true; + s_reader_sd_owner = xTaskGetCurrentTaskHandle(); + storage_claimed = true; + if (sdAdoptLiveMount()) storage = &SD; + else if (!sdRuntimeLifecycleBusy() && fmSdTryMount()) storage = &SD; +#elif defined(HAS_TANMATSU) || defined(HAS_TDISPLAY_P4) + s_reader_sd_busy = true; + s_reader_sd_owner = xTaskGetCurrentTaskHandle(); + storage_claimed = true; + if (tanSdTryMount()) storage = &SD_MMC; +#endif + if (!storage) { releaseStorage(); return ReaderLocalResult::NoStorage; } + + char path[300]; + snprintf(path, sizeof path, "%s", url + 3); + char* query = strchr(path, '?'); + if (query) *query = 0; + if (path[0] != '/') { releaseStorage(); return ReaderLocalResult::NotFound; } + + markSdIo(); + File file = storage->open(path, "r"); + if (!file || file.isDirectory()) { + if (file) file.close(); + releaseStorage(); + return ReaderLocalResult::NotFound; } - return 0; -} -// Extract attribute `attr` (e.g. "href") from a tag body h[0..len) into out. Handles -// quoted ("..", '..') and unquoted values. Returns true if found. -static bool readerTagAttr(const char* h, size_t len, const char* attr, char* out, size_t cap) { - size_t al = strlen(attr); - for (size_t i = 0; i + al + 1 < len; i++) { - if (!readerCiPrefix(h + i, len - i, attr)) continue; - size_t j = i + al; while (j < len && (h[j] == ' ' || h[j] == '\t')) j++; - if (j >= len || h[j] != '=') continue; - j++; while (j < len && (h[j] == ' ' || h[j] == '\t')) j++; - char q = 0; if (j < len && (h[j] == '"' || h[j] == '\'')) { q = h[j]; j++; } - size_t o = 0; - while (j < len && o + 1 < cap) { char c = h[j]; if (q ? (c == q) : (c == ' ' || c == '>' || c == '\t')) break; out[o++] = c; j++; } - out[o] = 0; return o > 0; - } - return false; -} -// Resolve href against base into out (absolute). Returns false for unusable schemes -// (#fragment, javascript:, mailto:, tel:, data:). -static bool readerResolveUrl(const char* base, const char* href, char* out, size_t cap) { - while (*href == ' ') href++; - size_t hl = strlen(href); - if (!hl || href[0] == '#') return false; - if (readerCiPrefix(href, hl, "javascript:") || readerCiPrefix(href, hl, "mailto:") || - readerCiPrefix(href, hl, "tel:") || readerCiPrefix(href, hl, "data:")) return false; - if (readerCiPrefix(href, hl, "http://") || readerCiPrefix(href, hl, "https://")) { snprintf(out, cap, "%s", href); } - else { - const char* se = strstr(base, "://"); if (!se) return false; - int sl = (int)(se - base); const char* host = se + 3; - const char* he = strchr(host, '/'); int hlen = he ? (int)(he - host) : (int)strlen(host); - if (href[0] == '/' && href[1] == '/') snprintf(out, cap, "%.*s:%s", sl, base, href); // //host/path - else if (href[0] == '/') snprintf(out, cap, "%.*s://%.*s%s", sl, base, hlen, host, href); // root-relative - else { // relative to base dir - const char* ls = strrchr(base, '/'); - if (ls && ls > se + 2) snprintf(out, cap, "%.*s%s", (int)(ls - base + 1), base, href); - else snprintf(out, cap, "%.*s://%.*s/%s", sl, base, hlen, host, href); + + bool read_failed = false; + while (*total < capacity && file.available()) { + const size_t request = ((capacity - *total) < 4096) ? (capacity - *total) : 4096; + const int read_count = file.read(raw + *total, request); + if (read_count <= 0) { read_failed = true; break; } + *total += (size_t)read_count; + if (generation == s_reader_generation && ((*total) & 0x3FFF) == 0) { + snprintf(s_reader_msg, sizeof s_reader_msg, "Opening SD page\xe2\x80\xa6 %uk", (unsigned)(*total / 1024)); + s_reader_dirty = true; } + vTaskDelay(1); } - char* frag = strchr(out, '#'); if (frag) *frag = 0; - return out[0] != 0; + file.close(); + releaseStorage(); + return read_failed ? ReaderLocalResult::ReadFailed : ReaderLocalResult::Ok; } -// One-pass HTML -> text. Drops comments + "; + char text[256]; + ReaderContent::Link links[4] = {}; + size_t link_count = 0; + const size_t text_len = ReaderContent::htmlToText( + html, strlen(html), text, sizeof text, "sd:/home.htm", + links, 4, &link_count); + + assert(text_len == strlen(text)); + assert(strstr(text, "Bookmarks") != nullptr); + assert(strstr(text, "CNN") != nullptr); + assert(strstr(text, "Local news & weather") != nullptr); + assert(strstr(text, "12345") == nullptr); + assert(link_count == 2); + assert(strcmp(links[0].href, "https://lite.cnn.com") == 0); + assert(strcmp(links[1].href, "sd:/news.htm") == 0); + assert(strncmp(text + links[0].start, "CNN", links[0].end - links[0].start) == 0); + assert(strncmp(text + links[1].start, "Local news", links[1].end - links[1].start) == 0); + + char resolved[240]; + assert(ReaderContent::resolveUrl("sd:/bookmarks/home.htm", "../index.htm", + resolved, sizeof resolved)); + assert(strcmp(resolved, "sd:/bookmarks/../index.htm") == 0); + assert(ReaderContent::resolveUrl("sd:/home.htm", "//text.npr.org", + resolved, sizeof resolved)); + assert(strcmp(resolved, "https://text.npr.org") == 0); + assert(!ReaderContent::resolveUrl("sd:/home.htm", "javascript:alert(1)", + resolved, sizeof resolved)); + + static const char attribute_html[] = + "Right"; + link_count = 0; + ReaderContent::htmlToText(attribute_html, strlen(attribute_html), text, sizeof text, + "sd:/home.htm", links, 4, &link_count); + assert(link_count == 1); + assert(strcmp(links[0].href, "sd:/right.htm") == 0); + return 0; +} \ No newline at end of file diff --git a/test/test_tdeck_keyboard_state.cpp b/test/test_tdeck_keyboard_state.cpp new file mode 100644 index 0000000..7357435 --- /dev/null +++ b/test/test_tdeck_keyboard_state.cpp @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "helpers/input/TDeckKeyboardState.h" + +static size_t update(TDeckKeyboardState& keyboard, uint8_t state[5], uint32_t now, + uint8_t* out) { + return keyboard.update(state, now, out, 8); +} + +static void setKey(uint8_t state[5], int col, int row, bool down) { + if (down) state[col] |= (uint8_t)(1U << row); + else state[col] &= (uint8_t)~(1U << row); +} + +int main() { + TDeckKeyboardState keyboard; + uint8_t state[5] = {}; + uint8_t out[8] = {}; + + setKey(state, 0, 2, true); // hold Sym + assert(update(keyboard, state, 10, out) == 0); + setKey(state, 0, 0, true); // q -> # + assert(update(keyboard, state, 20, out) == 1 && out[0] == '#'); + setKey(state, 0, 0, false); + update(keyboard, state, 30, out); + setKey(state, 0, 2, false); + update(keyboard, state, 40, out); + setKey(state, 0, 0, true); // hold use must not latch + assert(update(keyboard, state, 50, out) == 1 && out[0] == 'q'); + setKey(state, 0, 0, false); + update(keyboard, state, 60, out); + + setKey(state, 0, 2, true); // tap Sym + update(keyboard, state, 100, out); + setKey(state, 0, 2, false); + update(keyboard, state, 120, out); + setKey(state, 0, 1, true); // w -> 1 once + assert(update(keyboard, state, 130, out) == 1 && out[0] == '1'); + setKey(state, 0, 1, false); + update(keyboard, state, 140, out); + setKey(state, 0, 1, true); + assert(update(keyboard, state, 150, out) == 1 && out[0] == 'w'); + setKey(state, 0, 1, false); + update(keyboard, state, 160, out); + + setKey(state, 0, 2, true); // an unmapped symbol still uses the hold + update(keyboard, state, 170, out); + setKey(state, 0, 5, true); // Space has no T-Deck symbol mapping + assert(update(keyboard, state, 175, out) == 0); + setKey(state, 0, 5, false); + update(keyboard, state, 180, out); + setKey(state, 0, 2, false); + update(keyboard, state, 185, out); + setKey(state, 0, 0, true); + assert(update(keyboard, state, 190, out) == 1 && out[0] == 'q'); + setKey(state, 0, 0, false); + update(keyboard, state, 195, out); + + setKey(state, 0, 4, true); // held Alt preserves base layer + update(keyboard, state, 200, out); + setKey(state, 0, 0, true); + assert(update(keyboard, state, 210, out) == 1 && out[0] == 'q'); + setKey(state, 0, 0, false); + update(keyboard, state, 215, out); + setKey(state, 3, 4, true); // Alt+B stays controller-only + assert(update(keyboard, state, 216, out) == 0); + setKey(state, 3, 4, false); + update(keyboard, state, 217, out); + setKey(state, 2, 5, true); // Alt+C keeps its legacy control code + assert(update(keyboard, state, 218, out) == 1 && out[0] == 0x0C); + setKey(state, 2, 5, false); + update(keyboard, state, 219, out); + setKey(state, 0, 4, false); + update(keyboard, state, 220, out); + + setKey(state, 0, 4, true); // solo Alt is a one-shot symbol latch + update(keyboard, state, 225, out); + setKey(state, 0, 4, false); + update(keyboard, state, 230, out); + setKey(state, 0, 0, true); + assert(update(keyboard, state, 235, out) == 1 && out[0] == '#'); + setKey(state, 0, 0, false); + update(keyboard, state, 240, out); + + setKey(state, 0, 4, true); // double-tap Alt locks symbols + update(keyboard, state, 250, out); + setKey(state, 0, 4, false); + update(keyboard, state, 270, out); + setKey(state, 0, 4, true); + update(keyboard, state, 300, out); + setKey(state, 0, 4, false); + update(keyboard, state, 320, out); + setKey(state, 0, 0, true); + assert(update(keyboard, state, 330, out) == 1 && out[0] == '#'); + setKey(state, 0, 0, false); + update(keyboard, state, 340, out); + setKey(state, 0, 1, true); + assert(update(keyboard, state, 350, out) == 1 && out[0] == '1'); + setKey(state, 0, 1, false); + update(keyboard, state, 360, out); + setKey(state, 0, 4, true); // tap Alt unlocks + update(keyboard, state, 400, out); + setKey(state, 0, 4, false); + update(keyboard, state, 420, out); + + setKey(state, 0, 2, true); // double-tap Sym locks symbols + update(keyboard, state, 430, out); + setKey(state, 0, 2, false); + update(keyboard, state, 440, out); + setKey(state, 0, 2, true); + update(keyboard, state, 450, out); + setKey(state, 0, 2, false); + update(keyboard, state, 460, out); + setKey(state, 0, 1, true); + assert(update(keyboard, state, 470, out) == 1 && out[0] == '1'); + setKey(state, 0, 1, false); + update(keyboard, state, 475, out); + setKey(state, 0, 2, true); + update(keyboard, state, 480, out); + setKey(state, 0, 2, false); + update(keyboard, state, 490, out); + + setKey(state, 1, 6, true); // held Shift still uppercases base + update(keyboard, state, 500, out); + setKey(state, 0, 0, true); + assert(update(keyboard, state, 510, out) == 1 && out[0] == 'Q'); + + setKey(state, 0, 0, false); + setKey(state, 1, 6, false); + update(keyboard, state, 520, out); + setKey(state, 0, 2, true); // discarded physical hold + update(keyboard, state, 600, out); + keyboard.discardModifiers(); + setKey(state, 0, 2, false); + update(keyboard, state, 620, out); + setKey(state, 0, 0, true); + assert(update(keyboard, state, 630, out) == 1 && out[0] == 'q'); + setKey(state, 0, 0, false); + update(keyboard, state, 640, out); + + setKey(state, 0, 4, true); // discarded Alt hold cannot latch on release + update(keyboard, state, 650, out); + keyboard.discardModifiers(); + setKey(state, 0, 4, false); + update(keyboard, state, 660, out); + setKey(state, 0, 0, true); + assert(update(keyboard, state, 670, out) == 1 && out[0] == 'q'); + + setKey(state, 0, 0, false); + update(keyboard, state, 680, out); + setKey(state, 0, 2, true); // modifiers disabled: base key still routes + update(keyboard, state, 690, out); + setKey(state, 0, 0, true); + assert(keyboard.update(state, 700, out, 8, false) == 1 && out[0] == 'q'); + + uint8_t baseline[5] = {}; + setKey(baseline, 0, 1, true); // transition frame produces no edge later + keyboard.baseline(baseline); + assert(keyboard.update(baseline, 710, out, 8) == 0); + return 0; +} \ No newline at end of file diff --git a/test/test_utf8_text.cpp b/test/test_utf8_text.cpp new file mode 100644 index 0000000..3ccecf0 --- /dev/null +++ b/test/test_utf8_text.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include + +#include "ui-touch/Utf8Text.h" + +int main() { + const char full[] = "Ouderkerk\xE2\x98\x80\xEF\xB8\x8F"; + assert(strlen(full) == 15); + assert(Utf8Text::valid(full, strlen(full))); + + std::string broken(full, 14); // Wardrive's former name:sub(1, 14) + assert(!Utf8Text::valid(broken.data(), broken.size())); + + std::string scratch; + const char* clean = Utf8Text::sanitize(broken.data(), broken.size(), scratch); + assert(scratch == "Ouderkerk\xE2\x98\x80?"); + assert(Utf8Text::valid(clean, scratch.size())); + + scratch = "allocated"; + const char* unchanged = Utf8Text::sanitize(full, strlen(full), scratch); + assert(unchanged == full); + assert(scratch.empty()); + + const char overlong[] = { (char)0xC0, (char)0xAF }; + assert(!Utf8Text::valid(overlong, sizeof overlong)); + Utf8Text::sanitize(overlong, sizeof overlong, scratch); + assert(scratch == "?"); + return 0; +} \ No newline at end of file diff --git a/variants/tdisplay_p4/P4Audio.cpp b/variants/tdisplay_p4/P4Audio.cpp index c0a5a9e..a2658b8 100644 --- a/variants/tdisplay_p4/P4Audio.cpp +++ b/variants/tdisplay_p4/P4Audio.cpp @@ -89,6 +89,7 @@ bool p4AudioReady() { if (s_ready) return true; if (s_failed) return false; if (!s_mtx) s_mtx = xSemaphoreCreateMutex(); + if (!s_mtx) return false; xSemaphoreTake(s_mtx, portMAX_DELAY); if (s_ready || s_failed) { xSemaphoreGive(s_mtx); return s_ready; } @@ -125,6 +126,9 @@ bool p4AudioReady() { if (i2s_channel_init_std_mode(s_tx, &std_cfg) != ESP_OK || i2s_channel_enable(s_tx) != ESP_OK) { printf("[P4AUDIO] I2S init failed\n"); + i2s_channel_disable(s_tx); + i2s_del_channel(s_tx); + s_tx = nullptr; s_failed = true; xSemaphoreGive(s_mtx); return false; @@ -139,7 +143,7 @@ void p4AudioTone(int freq_hz, int duration_ms, int amplitude) { if (amplitude <= 0 || freq_hz <= 0 || duration_ms <= 0) return; if (!p4AudioReady()) return; if (amplitude > 30000) amplitude = 30000; - xSemaphoreTake(s_mtx, portMAX_DELAY); + if (xSemaphoreTake(s_mtx, 0) != pdTRUE) return; const int total = P4A_RATE * duration_ms / 1000; const int fade = P4A_RATE * 4 / 1000; // 4 ms in/out fade kills the click static int16_t buf[256 * 2]; // stereo frames, chunked @@ -163,4 +167,36 @@ void p4AudioTone(int freq_hz, int duration_ms, int amplitude) { xSemaphoreGive(s_mtx); } +uint32_t p4AudioStreamRate() { return P4A_RATE; } + +bool p4AudioStreamBegin() { + if (!p4AudioReady()) return false; + return xSemaphoreTake(s_mtx, portMAX_DELAY) == pdTRUE; +} + +bool p4AudioStreamWrite(const int16_t* samples, size_t frames) { + if (!samples || !frames || !s_tx) return false; + static int16_t stereo[256 * 2]; + while (frames) { + const size_t count = frames > 256 ? 256 : frames; + for (size_t i = 0; i < count; ++i) + stereo[2 * i] = stereo[2 * i + 1] = samples[i]; + size_t written = 0; + const size_t bytes = count * 2 * sizeof(int16_t); + if (i2s_channel_write(s_tx, stereo, bytes, &written, pdMS_TO_TICKS(300)) != ESP_OK || + written != bytes) return false; + samples += count; + frames -= count; + } + return true; +} + +void p4AudioStreamEnd() { + if (!s_mtx) return; + static const int16_t silence[256 * 2] = {}; + size_t written = 0; + i2s_channel_write(s_tx, silence, sizeof silence, &written, pdMS_TO_TICKS(300)); + xSemaphoreGive(s_mtx); +} + #endif // HAS_TDISPLAY_P4 diff --git a/variants/tdisplay_p4/P4Audio.h b/variants/tdisplay_p4/P4Audio.h index 46d9681..e1d1418 100644 --- a/variants/tdisplay_p4/P4Audio.h +++ b/variants/tdisplay_p4/P4Audio.h @@ -5,6 +5,7 @@ // DOUT=10. Fixed 16 kHz / 16-bit stereo — plenty for notification chimes, tiny buffers. // All calls are safe from any task; init is lazy on first use (~ms, I²C + I2S bring-up). #include +#include // Bring the codec + I2S up (idempotent). Returns false if the codec never answered. bool p4AudioReady(); @@ -12,3 +13,10 @@ bool p4AudioReady(); // Blocking sine tone: `amplitude` is the 16-bit peak (0..~30000; the UI uses pct*130). // No-op (fast) when amplitude <= 0 or the codec is absent. void p4AudioTone(int freq_hz, int duration_ms, int amplitude); + +// Exclusive fixed-rate PCM stream used by media playback. Samples are mono; +// the backend duplicates them to the codec's stereo I2S slots. +uint32_t p4AudioStreamRate(); +bool p4AudioStreamBegin(); +bool p4AudioStreamWrite(const int16_t* samples, size_t frames); +void p4AudioStreamEnd();