Merge branch 'main' of codeberg.org:cool-tech-zone/tangara-fw

custom
jacqueline 1 year ago
commit c5c6506ebc
  1. 3
      .luarc.json
  2. 3
      config.ld
  3. 13
      ldoc-stubs/alerts.lua
  4. 13
      ldoc-stubs/backstack.lua
  5. 13
      ldoc-stubs/bluetooth.lua
  6. 59
      ldoc-stubs/database.lua
  7. 19
      ldoc-stubs/playback.lua
  8. 18
      ldoc-stubs/power.lua
  9. 32
      ldoc-stubs/queue.lua
  10. 35
      ldoc-stubs/types.lua
  11. 14
      ldoc-stubs/volume.lua
  12. 6
      luals-stubs/alerts.lua
  13. 13
      luals-stubs/backstack.lua
  14. 8
      luals-stubs/bluetooth.lua
  15. 12
      luals-stubs/controls.lua
  16. 31
      luals-stubs/database.lua
  17. 9
      luals-stubs/display.lua
  18. 1546
      luals-stubs/lvgl.lua
  19. 3
      luals-stubs/playback.lua
  20. 3
      luals-stubs/power.lua
  21. 18
      luals-stubs/queue.lua
  22. 25
      luals-stubs/screen.lua
  23. 12
      luals-stubs/time.lua
  24. 12
      luals-stubs/types.lua
  25. 7
      luals-stubs/volume.lua
  26. 43
      src/audio/audio_fsm.cpp
  27. 1
      src/audio/include/audio_events.hpp
  28. 2
      src/audio/track_queue.cpp
  29. 6
      src/database/database.cpp
  30. 202
      tools/luals-gendoc/gendoc.lua
  31. 388
      tools/luals-gendoc/json.lua

@ -1,7 +1,6 @@
{ {
"$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json",
"workspace.library": ["lib/luavgl/src", "luals-stubs"], "workspace.library": ["luals-stubs"],
"workspace.ignoreDir": ["ldoc-stubs"],
"runtime.version": "Lua 5.4", "runtime.version": "Lua 5.4",
} }

@ -1,3 +0,0 @@
file = {'ldoc-stubs'}
project = "Tangara"
description = "Lua modules provided by Tangara's firmware"

@ -1,13 +0,0 @@
--- Module for showing transient popups over the current screen.
-- @module alerts
local alerts = {}
--- Shows a new alert, replacing any already visible alerts.
-- @tparam function constructor Called to create the UI for the alert. A new default root object and group will be set before calling this function.i Alerts are non-interactable; the group created for the constructor will not be granted focus.
function alerts.show(constructor) end
--- Dismisses any visible alerts, removing them from the screen.
function alerts.hide() end
return alerts

@ -1,13 +0,0 @@
--- Module for adding and removing screens from the system's backstack.
-- @module backstack
local backstack = {}
--- Pushes a new screen onto the backstack.
-- @tparam function constructor Called to create the UI for the new screen. A new default root object and group will be set before calling this function. The function provided should return a table holding any bindings used by this screen; the returned value is retained so long as this screen is present in the backstack.
function backstack.push(constructor) end
--- Removes the currently active screen, and instead shows the screen underneath it on the backstack. Does nothing if this is the only existing screen.
function backstack.pop() end
return backstack

@ -1,13 +0,0 @@
--- Properties and functions for handling Bluetooth connectivity
-- @module bluetooth
local bluetooth = {}
--- Whether or not the Bluetooth stack is currently enabled. This property is writeable, and can be used to enable or disable Bluetooth.
-- @see types.Property
bluetooth.enabled = types.Property
--- Whether or not there is an active connection to another Bluetooth device.
-- @see types.Property
bluetooth.connected = types.Property
return bluetooth

@ -1,59 +0,0 @@
--- Module for accessing and updating data about the user's library of tracks.
-- @module database
local database = {}
--- Returns a list of all indexes in the database.
-- @treturn Array(Index)
function database.indexes() end
--- An iterator is a userdata type that behaves like an ordinary Lua iterator.
-- @type Iterator
local Iterator = {}
--- A TrackId is a unique identifier, representing a playable track in the
--- user's library.
-- @type TrackId
local TrackId = {}
--- A record is an item within an Index, representing some value at a specific
--- depth.
-- @type Record
local Record = {}
--- Gets the human-readable text representing this record. The `__tostring`
--- metatable function is an alias of this function.
-- @treturn string
function Record:title() end
--- Returns the value that this record represents. This may be either a track
--- id, for records which uniquely identify a track, or it may be a new
--- Iterator representing the next level of depth for the current index.
---
--- For example, each Record in the "All Albums" index corresponds to an entire
--- album of tracks; the 'contents' of such a Record is an iterator returning
--- each track in the album represented by the Record. The contents of each of
--- the returned 'track' Records would be a full Track, as there is no further
--- disambiguation needed.
-- @treturn TrackId|Iterator(Record)
function Record:contents() end
--- An index is heirarchical, sorted, view of the tracks within the database.
--- For example, the 'All Albums' index contains, first, a sorted list of every
--- album name in the library. Then, at the second level of the index, a sorted
--- list of every track within each album.
-- @type Index
local Index = {}
--- Gets the human-readable name of this index. This is typically something
--- like "All Albums", or "Albums by Artist". The `__tostring` metatable
--- function is an alias of this function.
-- @treturn string
function Index:name() end
--- Returns a new iterator that can be used to access every record within the
--- first level of this index.
-- @treturn Iterator(Record)
function Index:iter() end
return database

@ -1,19 +0,0 @@
--- Properties for interacting with the audio playback system
-- @module playback
local playback = {}
--- Whether or not any audio is *allowed* to be played. If there is a current track, then this is essentially an indicator of whether playback is paused or unpaused.
-- @see types.Property
playback.playing = types.Property
--- Rich information about the currently playing track.
-- @see types.Property
-- @see types.Track
playback.track = types.Property
--- The current playback position within the current track, in seconds.
-- @see types.Property
playback.position = types.Property
return playback

@ -1,18 +0,0 @@
--- Properties and functions that deal with the device's battery and power state
-- @module power
local power = {}
--- The battery's current charge as a percentage
-- @see types.Property
power.battery_pct = types.Property
--- The battery's current voltage, in millivolts.
-- @see types.Property
power.battery_millivolts = types.Property
--- Whether or not the device is currently receiving external power
-- @see types.Property
power.plugged_in = types.Property
return power

@ -1,32 +0,0 @@
--- Properties and functions for inspecting and manipulating the track playback queue
-- @module queue
local queue = {}
--- The index in the queue of the currently playing track. This may be zero if the queue is empty.
-- @see types.Property
queue.position = types.Property
--- The total number of tracks in the queue, including tracks which have already been played.
-- @see types.Property
queue.size = types.Property
--- Determines whether or not the queue will be restarted after the final track is played.
-- @see types.Property
queue.replay = types.Property
-- Determines whether or not the current track will repeat indefinitely
-- @see types.Property
queue.repeat_track = types.Property
--- Determines whether, when progressing to the next track in the queue, the next track will be chosen randomly. The random selection algorithm used is a Miller Shuffle, which guarantees that no repeat selections will be made until every item in the queue has been played.
-- @see types.Property
queue.random = types.Property
--- Moves forward in the play queue, looping back around to the beginning if repeat is on.
function queue.next() end
--- Moves backward in the play queue, looping back around to the end if repeat is on.
function queue.previous() end
return queue

@ -1,35 +0,0 @@
--- Userdata-based types used throughout the rest of the API. These types are
--- not generally constructable within Lua code.
-- @module types
local types = {}
--- A observable value, owned by the C++ firmware.
-- @type Property
types.Property = {}
--- Gets the current value
-- @return The property's current value.
function Property:get() end
--- Sets a new value. Not all properties may be set from within Lua code. For
--- example, it makes little sense to attempt to override the current battery
--- level.
-- @param val The new value. This should generally be of the same type as the existing value.
-- @return true if the new value was applied, or false if the backing C++ code rejected the new value (e.g. if it was out of range, or the wrong type).
function Property:set(val) end
--- Invokes the given function once immediately with the current value, and then again whenever the value changes.
--- The function is invoked for *all* changes; both from the underlying C++ data, and from calls to `set` (if this is a Lua-writeable property).
--- The binding will be active **only** so long as the given function remains in scope.
-- @param fn callback function to apply property values. Must accept one argument; the updated value.
-- @return fn, for more ergonmic use with anonymous closures.
function Property:bind(fn) end
--- Table containing information about a track. Most fields are optional.
-- @type Track
types.Track = {}
--- The title of the track, or the filename if no title is available.
types.Track.title = ""
return Property

@ -1,14 +0,0 @@
--- Module for interacting with playback volume. The Bluetooth and wired outputs store their current volume separately; this API only allows interacting with the volume of the currently used output device.
-- @module volume
local volume = {}
--- The current volume as a percentage of the current volume limit.
-- @see types.Property
volume.current_pct = types.Property
--- The current volume in terms of decibels relative to line level.
-- @see types.Property
volume.current_db = types.Property
return volume

@ -1,11 +1,15 @@
--- @meta --- @meta
--- The `alerts` module contains functions for showing transient popups over
--- the current screen.
--- @class alerts --- @class alerts
local alerts = {} local alerts = {}
--- @param constructor function --- Shows a new alert, replacing any other alerts.
--- @param constructor function Called to create the UI for the alert. A new default root object and group will be set before calling this function.i Alerts are non-interactable; the group created for the constructor will not be granted focus.
function alerts.show(constructor) end function alerts.show(constructor) end
--- Dismisses any visible alerts, removing them from the screen.
function alerts.hide() end function alerts.hide() end
return alerts return alerts

@ -1,11 +1,20 @@
--- @meta --- @meta
--- The `backstack` module contains functions that can be used to implement a
--- basic stack-based navigation hierarchy. See also the `screen` module, which
--- provides a class prototype meant for use with this module.
--- @class backstack --- @class backstack
local backstack = {} local backstack = {}
--- @param constructor function --- Displays the given screen to the user. If there was already a screen being
function backstack.push(constructor) end --- displayed, then the current screen is removed from the display, and added
--- to the backstack.
--- @param screen screen The screen to display.
function backstack.push(screen) end
--- Removes the current screen from the display, then replaces it with the
--- screen that is at the top of the backstack. This function does nothing if
--- there are no other screens in the stack.
function backstack.pop() end function backstack.pop() end
return backstack return backstack

@ -1,8 +1,12 @@
--- @meta --- @meta
--- The 'bluetooth' module contains Properties and functions for interacting
--- with the device's Bluetooth capabilities.
--- @class bluetooth --- @class bluetooth
--- @field enabled Property --- @field enabled Property Whether or not the Bluetooth stack is currently enabled. This property is writeable, and can be used to enable or disable Bluetooth.
--- @field connected Property --- @field connected Property Whether or not there is an active connection to another Bluetooth device.
--- @field paired_device Property The device that is currently paired. The bluetooth stack will automatically connected to this device if possible.
--- @field devices Property Devices nearby that have been discovered.
local bluetooth = {} local bluetooth = {}
return bluetooth return bluetooth

@ -0,0 +1,12 @@
--- @meta
--- The `controls` module contains Properties relating to the device's physical
--- controls. These controls include the touchwheel, the lock switch, and the
--- side buttons.
--- @class controls
--- @field scheme Property The currently configured control scheme
--- @field scroll_sensitivity Property How much rotational motion is required on the touchwheel per scroll tick.
--- @field lock_switch Property The current state of the device's lock switch.
local controls = {}
return controls

@ -1,33 +1,60 @@
--- @meta --- @meta
--- The `database` module contains Properties and functions for working with
--- the device's LevelDB-backed track database.
--- @class database --- @class database
--- @field updating Property Whether or not a database re-index is currently in progress.
local database = {} local database = {}
--- Returns a list of all indexes in the database.
--- @return Index[] --- @return Index[]
function database.indexes() end function database.indexes() end
--- An iterator is a userdata type that behaves like an ordinary Lua iterator.
--- @class Iterator --- @class Iterator
local Iterator = {} local Iterator = {}
--- A TrackId is a unique identifier, representing a playable track in the
--- user's library.
--- @class TrackId --- @class TrackId
local TrackId = {} local TrackId = {}
--- Gets the human-readable text representing this record. The `__tostring`
--- metatable function is an alias of this function.
--- @class Record --- @class Record
local Record = {} local Record = {}
--- @return string --- @return string
function Record:title() end function Record:title() end
--- @return TrackId|Iterator(Record) --- Returns the value that this record represents. This may be either a track
--- id, for records which uniquely identify a track, or it may be a new
--- Iterator representing the next level of depth for the current index.
---
--- For example, each Record in the "All Albums" index corresponds to an entire
--- album of tracks; the 'contents' of such a Record is an iterator returning
--- each track in the album represented by the Record. The contents of each of
--- the returned 'track' Records would be a full Track, as there is no further
--- disambiguation needed.
--- @return TrackId|Iterator r A track id if this is a leaf record, otherwise a new iterator for the next level of this index.
function Record:contents() end function Record:contents() end
--- An index is heirarchical, sorted, view of the tracks within the database.
--- For example, the 'All Albums' index contains, first, a sorted list of every
--- album name in the library. Then, at the second level of the index, a sorted
--- list of every track within each album.
--- @class Index --- @class Index
local Index = {} local Index = {}
--- Gets the human-readable name of this index. This is typically something
--- like "All Albums", or "Albums by Artist". The `__tostring` metatable
--- function is an alias of this function.
--- @return string --- @return string
function Index:name() end function Index:name() end
--- @return Iterator(Record) --- Returns a new iterator that can be used to access every record within the
--- first level of this index.
--- @return Iterator it An iterator that yields `Record`s.
function Index:iter() end function Index:iter() end
return database return database

@ -0,0 +1,9 @@
--- @meta
--- The `display` module contains Properties relating to the device's physical
--- display.
--- @class display
--- @field brightness Property The screen's current brightness, as a gamma-corrected percentage value from 0 to 100.
local display = {}
return display

File diff suppressed because it is too large Load Diff

@ -1,6 +1,7 @@
--- @meta --- @meta
--- Properties for interacting with the audio playback system --- The `playback` module contains Properties and functions for interacting
--- the device's audio pipeline.
--- @class playback --- @class playback
--- @field playing Property Whether or not audio is allowed to be played. if there is a current track, then this indicated whether playback is paused or unpaused. If there is no current track, this determines what will happen when the first track is added to the queue. --- @field playing Property Whether or not audio is allowed to be played. if there is a current track, then this indicated whether playback is paused or unpaused. If there is no current track, this determines what will happen when the first track is added to the queue.
--- @field track Property The currently playing track. --- @field track Property The currently playing track.

@ -1,6 +1,7 @@
--- @meta --- @meta
--- Properties and functions that deal with the device's battery and power state. --- The `power` module contains properties and functions that relate to the
--- device's battery and charging state.
--- @class power --- @class power
--- @field battery_pct Property The battery's current charge, as a percentage of the maximum charge. --- @field battery_pct Property The battery's current charge, as a percentage of the maximum charge.
--- @field battery_millivolts Property The battery's current voltage, in millivolts. --- @field battery_millivolts Property The battery's current voltage, in millivolts.

@ -1,6 +1,10 @@
--- @meta --- @meta
--- Properties and functions for inspecting and manipulating the track playback queue --- The `queue` module contains Properties and functions that relate to the
--- device's playback queue. This is a persistent, disk-backed list of TrackIds
--- that includes the currently playing track, tracks that have been played,
--- and tracks that are scheduled to be played after the current track has
--- finished.
--- @class queue --- @class queue
--- @field position Property The index in the queue of the currently playing track. This may be zero if the queue is empty. Writeable. --- @field position Property The index in the queue of the currently playing track. This may be zero if the queue is empty. Writeable.
--- @field size Property The total number of tracks in the queue, including tracks which have already been played. --- @field size Property The total number of tracks in the queue, including tracks which have already been played.
@ -9,7 +13,19 @@
--- @field random Property Determines whether, when progressing to the next track in the queue, the next track will be chosen randomly. The random selection algorithm used is a Miller Shuffle, which guarantees that no repeat selections will be made until every item in the queue has been played. Writeable. --- @field random Property Determines whether, when progressing to the next track in the queue, the next track will be chosen randomly. The random selection algorithm used is a Miller Shuffle, which guarantees that no repeat selections will be made until every item in the queue has been played. Writeable.
local queue = {} local queue = {}
--- Adds the given track or database iterator to the end of the queue. Database
--- iterators passed to this method will be unnested and expanded into the track
--- ids they contain.
--- @param val TrackId|Iterator
function queue.add(val) end
--- Removes all tracks from the queue.
function queue.clear() end
--- Moves forward in the play queue, looping back around to the beginning if repeat is on.
function queue.next() end function queue.next() end
--- Moves backward in the play queue, looping back around to the end if repeat is on.
function queue.previous() end function queue.previous() end
return queue return queue

@ -0,0 +1,25 @@
--- @meta
--- A distinct full-screen UI. Each screen has an associated LVGL UI tree and
--- group, and can be shown on-screen using the 'backstack' module.
--- Screens make use of prototype inheritance in order to provide a consistent
--- interface for the C++ firmware to work with.
--- See [Programming in Lua, chapter 16](https://www.lua.org/pil/16.2.html).
--- @class screen
local screen = {}
--- Creates a new screen instance.
function screen:new(params) end
--- Called just before this screen is first displayed to the user.
function screen:createUi() end
--- Called whenever this screen is displayed to the user.
function screen:onShown() end
--- Called whenever this screen is being hidden by the user; either because a
--- new screen is being pushed on top of this way, or because this screen has
--- been popped off of the stack.
function screen:onHidden() end
return screen

@ -0,0 +1,12 @@
--- @meta
--- The `time` module contains functions for dealing with the current time
--- since boot.
--- @class time
local time = {}
--- Returns the time in milliseconds since the device booted.
--- @return integer
function time.ticks() end
return time

@ -1,13 +1,23 @@
--- @meta --- @meta
--- A observable value, owned by the C++ firmware.
---@class Property ---@class Property
local property = {} local property = {}
--- @return integer|string|table|boolean val Returns the property's current value
function property:get() end function property:get() end
--- Sets a new value. Not all properties may be set from within Lua code. For
--- example, it makes little sense to attempt to override the current battery
--- level.
--- @param val? integer|string|table|boolean The new value. Optional; if not argument is passed, the property will be set to 'nil'.
--- @return boolean success whether or not the new value was accepted
function property:set(val) end function property:set(val) end
--- @param fn function --- Invokes the given function once immediately with the current value, and then again whenever the value changes.
--- The function is invoked for *all* changes; both from the underlying C++ data, and from calls to `set` (if this is a Lua-writeable property).
--- The binding will be active **only** so long as the given function remains in scope.
--- @param fn function callback to apply property values. Must accept one argument; the updated value.
function property:bind(fn) end function property:bind(fn) end
return property return property

@ -1,8 +1,11 @@
--- @meta --- @meta
--- Module for interacting with playback volume. The Bluetooth and wired outputs store their current volume separately; this API only allows interacting with the volume of the currently used output device.
--- @class volume --- @class volume
--- @field current_pct Property --- @field current_pct Property The current volume as a percentage of the current volume limit.
--- @field current_db Property --- @field current_db Property The current volume in terms of decibels relative to line level (only applicable to headphone output)
--- @field left_bias Property An additional modifier in decibels to apply to the left channel (only applicable to headphone output)
--- @field limit_db Property The maximum allowed output volume, in terms of decibels relative to line level (only applicable to headphone output)
local volume = {} local volume = {}
return volume return volume

@ -13,6 +13,8 @@
#include "audio_sink.hpp" #include "audio_sink.hpp"
#include "bluetooth_types.hpp" #include "bluetooth_types.hpp"
#include "cppbor.h"
#include "cppbor_parse.h"
#include "esp_heap_caps.h" #include "esp_heap_caps.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
@ -58,6 +60,8 @@ StreamBufferHandle_t AudioState::sDrainBuffer;
std::optional<database::TrackId> AudioState::sCurrentTrack; std::optional<database::TrackId> AudioState::sCurrentTrack;
bool AudioState::sIsPlaybackAllowed; bool AudioState::sIsPlaybackAllowed;
static std::optional<std::pair<std::string, uint32_t>> sLastTrackUpdate;
void AudioState::react(const system_fsm::BluetoothEvent& ev) { void AudioState::react(const system_fsm::BluetoothEvent& ev) {
if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) {
return; return;
@ -310,11 +314,15 @@ void Standby::react(const QueueUpdate& ev) {
if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) {
return; return;
} }
if (ev.reason == QueueUpdate::Reason::kDeserialised && sLastTrackUpdate) {
return;
}
clearDrainBuffer(); clearDrainBuffer();
playTrack(*current_track); playTrack(*current_track);
} }
static const char kQueueKey[] = "audio:queue"; static const char kQueueKey[] = "audio:queue";
static const char kCurrentFileKey[] = "audio:current";
void Standby::react(const system_fsm::KeyLockChanged& ev) { void Standby::react(const system_fsm::KeyLockChanged& ev) {
if (!ev.locking) { if (!ev.locking) {
@ -332,6 +340,14 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) {
return; return;
} }
db->put(kQueueKey, queue.serialise()); db->put(kQueueKey, queue.serialise());
if (sLastTrackUpdate) {
cppbor::Array current_track{
cppbor::Tstr{sLastTrackUpdate->first},
cppbor::Uint{sLastTrackUpdate->second},
};
db->put(kCurrentFileKey, current_track.toString());
}
}); });
} }
@ -341,13 +357,32 @@ void Standby::react(const system_fsm::StorageMounted& ev) {
if (!db) { if (!db) {
return; return;
} }
auto res = db->get(kQueueKey);
if (res) { // Restore the currently playing file before restoring the queue. This way,
// we can fall back to restarting the queue's current track if there's any
// issue restoring the current file.
auto current = db->get(kCurrentFileKey);
if (current) {
// Again, ensure we don't boot-loop by trying to play a track that causes
// a crash over and over again.
db->put(kCurrentFileKey, "");
auto [parsed, unused, err] = cppbor::parse(
reinterpret_cast<uint8_t*>(current->data()), current->size());
if (parsed->type() == cppbor::ARRAY) {
std::string filename = parsed->asArray()->get(0)->asTstr()->value();
uint32_t pos = parsed->asArray()->get(1)->asUint()->value();
sLastTrackUpdate = std::make_pair(filename, pos);
sFileSource->SetPath(filename, pos);
}
}
auto queue = db->get(kQueueKey);
if (queue) {
// Don't restore the same queue again. This ideally should do nothing, // Don't restore the same queue again. This ideally should do nothing,
// but guards against bad edge cases where restoring the queue ends up // but guards against bad edge cases where restoring the queue ends up
// causing a crash. // causing a crash.
db->put(kQueueKey, ""); db->put(kQueueKey, "");
sServices->track_queue().deserialise(*res); sServices->track_queue().deserialise(*queue);
} }
}); });
} }
@ -399,6 +434,7 @@ void Playback::react(const QueueUpdate& ev) {
void Playback::react(const PlaybackUpdate& ev) { void Playback::react(const PlaybackUpdate& ev) {
ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed,
ev.track->duration); ev.track->duration);
sLastTrackUpdate = std::make_pair(ev.track->filepath, ev.seconds_elapsed);
} }
void Playback::react(const internal::InputFileOpened& ev) {} void Playback::react(const internal::InputFileOpened& ev) {}
@ -407,6 +443,7 @@ void Playback::react(const internal::InputFileClosed& ev) {}
void Playback::react(const internal::InputFileFinished& ev) { void Playback::react(const internal::InputFileFinished& ev) {
ESP_LOGI(kTag, "finished playing file"); ESP_LOGI(kTag, "finished playing file");
sLastTrackUpdate.reset();
sServices->track_queue().finish(); sServices->track_queue().finish();
if (!sServices->track_queue().current()) { if (!sServices->track_queue().current()) {
for (int i = 0; i < 20; i++) { for (int i = 0; i < 20; i++) {

@ -44,6 +44,7 @@ struct QueueUpdate : tinyfsm::Event {
kExplicitUpdate, kExplicitUpdate,
kRepeatingLastTrack, kRepeatingLastTrack,
kTrackFinished, kTrackFinished,
kDeserialised,
}; };
Reason reason; Reason reason;
}; };

@ -486,7 +486,7 @@ auto TrackQueue::deserialise(const std::string& s) -> void {
QueueParseClient client{*this}; QueueParseClient client{*this};
const uint8_t* data = reinterpret_cast<const uint8_t*>(s.data()); const uint8_t* data = reinterpret_cast<const uint8_t*>(s.data());
cppbor::parse(data, data + s.size(), &client); cppbor::parse(data, data + s.size(), &client);
notifyChanged(true, Reason::kExplicitUpdate); notifyChanged(true, Reason::kDeserialised);
} }
} // namespace audio } // namespace audio

@ -229,13 +229,17 @@ auto Database::sizeOnDiskBytes() -> size_t {
} }
auto Database::put(const std::string& key, const std::string& val) -> void { auto Database::put(const std::string& key, const std::string& val) -> void {
if (val.empty()) {
db_->Delete(leveldb::WriteOptions{}, kKeyCustom + key);
} else {
db_->Put(leveldb::WriteOptions{}, kKeyCustom + key, val); db_->Put(leveldb::WriteOptions{}, kKeyCustom + key, val);
} }
}
auto Database::get(const std::string& key) -> std::optional<std::string> { auto Database::get(const std::string& key) -> std::optional<std::string> {
std::string val; std::string val;
auto res = db_->Get(leveldb::ReadOptions{}, kKeyCustom + key, &val); auto res = db_->Get(leveldb::ReadOptions{}, kKeyCustom + key, &val);
if (!res.ok()) { if (!res.ok() || val.empty()) {
return {}; return {};
} }
return val; return val;

@ -0,0 +1,202 @@
#!/usr/bin/env lua
local json = require "json"
if #arg > 0 then
print("usage:", arg[0])
print([[
reads a lua-language-server json doc output from stdin, converts it into
markdown, and writes the result to stdout]])
return
end
local raw_data = io.read("*all")
local parsed = json.decode(raw_data)
local definitions_per_module = {}
local fields_per_class = {}
for _, class in ipairs(parsed) do
if not class.defines or not class.defines[1] then goto continue end
-- Filter out any definitions that didn't come from us.
local path = class.defines[1].file
if not string.find(path, "/luals-stubs/", 1, true) then goto continue end
local module_name = string.gsub(path, ".*/(%a*)%.lua", "%1")
local module = definitions_per_module[module_name] or {}
module[class.name] = class
definitions_per_module[module_name] = module
local fields = {}
for _, field in ipairs(class.fields or {}) do
fields[field.name] = true
end
fields_per_class[class.name] = fields
::continue::
end
local function sortedPairs(t)
local keys = {}
for key in pairs(t) do
table.insert(keys, key)
end
table.sort(keys)
local generator = coroutine.create(function()
for _, key in ipairs(keys) do
coroutine.yield(key, t[key])
end
end)
return function()
local _, key, val = coroutine.resume(generator)
return key, val
end
end
local function printHeading(level, text)
local hashes = ""
for _ = 1, level do hashes = hashes .. "#" end
print(hashes .. " " .. text)
end
local function filterArgs(field)
if not field.extends.args then return {} end
local ret = {}
for _, arg in ipairs(field.extends.args) do
if arg.name ~= "self" then table.insert(ret, arg) end
end
return ret
end
local function filterReturns(field)
if not field.extends.returns then return {} end
local ret = {}
for _, r in ipairs(field.extends.returns) do
if r.desc then table.insert(ret, r) end
end
return ret
end
local function emitField(level, prefix, field)
if not field.desc then return end
printHeading(level, "`" .. prefix .. "." .. field.name .. "`")
print()
print("`" .. field.extends.view .. "`")
print()
if field.rawdesc then
print(field.rawdesc)
print()
end
local args = filterArgs(field)
if #args > 0 then
printHeading(level + 1, "Arguments")
print()
for _, arg in ipairs(args) do
print(string.format(" - *%s*: %s", arg.name, arg.desc))
end
print()
end
local rets = filterReturns(field)
if #rets > 0 then
printHeading(level + 1, "Returns")
print()
for _, ret in ipairs(rets) do
if #rets > 1 then
print(" - " .. ret.desc)
else
print(ret.desc)
end
end
print()
end
end
local function baseClassName(class)
for _, define in ipairs(class.defines or {}) do
for _, extend in ipairs(define.extends or {}) do
if extend.type == "doc.extends.name" then
return extend.view
end
end
end
end
local function isEnum(class)
for _, define in pairs(class.defines) do
if define.type == "doc.enum" then return true end
end
return false
end
local function isAlias(class)
for _, define in pairs(class.defines) do
if define.type == "doc.alias" then return true end
end
return false
end
local function emitClass(level, prefix, class)
if not class.name then return end
if not class.fields then return end
if isAlias(class) then return end
for _, define in ipairs(class.defines or {}) do
if define.type == "tablefield" then
print(" - " .. class.name)
return
end
end
printHeading(level, "`" .. prefix .. "." .. class.name .. "`")
print()
local base_class = baseClassName(class)
local base_class_fields = {}
if base_class then
base_class_fields = fields_per_class[base_class] or {}
print("`" .. class.name .. ":" .. base_class .. "`")
print()
end
if class.desc then print(class.desc) end
for _, field in ipairs(class.fields or {}) do
if not base_class_fields[field.name] then
emitField(level + 1, class.name, field)
end
end
if isEnum(class) then
printHeading(level + 1, "Values")
print()
end
end
local initial_level = 3
for name, module in sortedPairs(definitions_per_module) do
printHeading(initial_level, "`" .. name .. "`")
local top_level_class = module[name]
if top_level_class then
if top_level_class.desc then print(top_level_class.desc) end
for _, field in ipairs(top_level_class.fields or {}) do
emitField(initial_level + 1, name, field)
end
end
for _, class in sortedPairs(module) do
if class.name ~= name then
emitClass(initial_level + 1, name, class)
end
end
end

@ -0,0 +1,388 @@
--
-- json.lua
--
-- Copyright (c) 2020 rxi
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
-- this software and associated documentation files (the "Software"), to deal in
-- the Software without restriction, including without limitation the rights to
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-- of the Software, and to permit persons to whom the Software is furnished to do
-- so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
--
local json = { _version = "0.1.2" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\",
[ "\"" ] = "\"",
[ "\b" ] = "b",
[ "\f" ] = "f",
[ "\n" ] = "n",
[ "\r" ] = "r",
[ "\t" ] = "t",
}
local escape_char_map_inv = { [ "/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte()))
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(val, 1) ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
local line_count = 1
local col_count = 1
for i = 1, idx - 1 do
col_count = col_count + 1
if str:sub(i, i) == "\n" then
line_count = line_count + 1
col_count = 1
end
end
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(1, 4), 16 )
local n2 = tonumber( s:sub(7, 10), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local res = ""
local j = i + 1
local k = j
while j <= #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
elseif x == 92 then -- `\`: Escape
res = res .. str:sub(k, j - 1)
j = j + 1
local c = str:sub(j, j)
if c == "u" then
local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1)
or str:match("^%x%x%x%x", j + 1)
or decode_error(str, j - 1, "invalid unicode escape in string")
res = res .. parse_unicode_escape(hex)
j = j + #hex
else
if not escape_chars[c] then
decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string")
end
res = res .. escape_char_map_inv[c]
end
k = j + 1
elseif x == 34 then -- `"`: End of string
res = res .. str:sub(k, j - 1)
return res, j + 1
end
j = j + 1
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
local res, idx = parse(str, next_char(str, 1, space_chars, true))
idx = next_char(str, idx, space_chars, true)
if idx <= #str then
decode_error(str, idx, "trailing garbage")
end
return res
end
return json
Loading…
Cancel
Save