diff --git a/.env.ps1 b/.env.ps1 new file mode 100644 index 00000000..26fe4c34 --- /dev/null +++ b/.env.ps1 @@ -0,0 +1,4 @@ +$repo_dir="$(pwd)".replace("`\", "/") +$env:PROJ_PATH="$repo_dir" +$env:IDF_PATH="$repo_dir/lib/esp-idf" +. "$($env:IDF_PATH)/export.ps1" diff --git a/.luarc.json b/.luarc.json index a0e9e1b7..e16ba477 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,7 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/sumneko/vscode-lua/master/setting/schema.json", - "workspace.library": ["lib/luavgl/src", "luals-stubs"], - "workspace.ignoreDir": ["ldoc-stubs"], + "workspace.library": ["luals-stubs"], "runtime.version": "Lua 5.4", } diff --git a/config.ld b/config.ld deleted file mode 100644 index e1c54172..00000000 --- a/config.ld +++ /dev/null @@ -1,3 +0,0 @@ -file = {'ldoc-stubs'} -project = "Tangara" -description = "Lua modules provided by Tangara's firmware" diff --git a/ldoc-stubs/alerts.lua b/ldoc-stubs/alerts.lua deleted file mode 100644 index 6fecdd7c..00000000 --- a/ldoc-stubs/alerts.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/backstack.lua b/ldoc-stubs/backstack.lua deleted file mode 100644 index d4807d37..00000000 --- a/ldoc-stubs/backstack.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/bluetooth.lua b/ldoc-stubs/bluetooth.lua deleted file mode 100644 index 3160ef7e..00000000 --- a/ldoc-stubs/bluetooth.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/database.lua b/ldoc-stubs/database.lua deleted file mode 100644 index 97359ab1..00000000 --- a/ldoc-stubs/database.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/playback.lua b/ldoc-stubs/playback.lua deleted file mode 100644 index 07ed65f6..00000000 --- a/ldoc-stubs/playback.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/power.lua b/ldoc-stubs/power.lua deleted file mode 100644 index 466cafed..00000000 --- a/ldoc-stubs/power.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/queue.lua b/ldoc-stubs/queue.lua deleted file mode 100644 index b3000040..00000000 --- a/ldoc-stubs/queue.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/types.lua b/ldoc-stubs/types.lua deleted file mode 100644 index 3480610c..00000000 --- a/ldoc-stubs/types.lua +++ /dev/null @@ -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 diff --git a/ldoc-stubs/volume.lua b/ldoc-stubs/volume.lua deleted file mode 100644 index 7eff24c5..00000000 --- a/ldoc-stubs/volume.lua +++ /dev/null @@ -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 diff --git a/lua/browser.lua b/lua/browser.lua index 055c8641..fa412021 100644 --- a/lua/browser.lua +++ b/lua/browser.lua @@ -7,30 +7,11 @@ local playing = require("playing") local styles = require("styles") local playback = require("playback") local theme = require("theme") +local screen = require("screen") -local browser = {} - -function browser.create(opts) - local screen = {} - screen.root = lvgl.Object(nil, { - flex = { - flex_direction = "column", - flex_wrap = "wrap", - justify_content = "flex-start", - align_items = "flex-start", - align_content = "flex-start", - }, - w = lvgl.HOR_RES(), - h = lvgl.VER_RES(), - }) - screen.root:center() - - screen.status_bar = widgets.StatusBar(screen.root, { - title = opts.title, - }) - - if opts.breadcrumb then - local header = screen.root:Object { +return screen:new { + createUi = function(self) + self.root = lvgl.Object(nil, { flex = { flex_direction = "column", flex_wrap = "wrap", @@ -39,101 +20,114 @@ function browser.create(opts) align_content = "flex-start", }, w = lvgl.HOR_RES(), - h = lvgl.SIZE_CONTENT, - pad_left = 4, - pad_right = 4, - pad_bottom = 2, - bg_opa = lvgl.OPA(100), - scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, - } + h = lvgl.VER_RES(), + }) + self.root:center() + + self.status_bar = widgets.StatusBar(self.root, { + title = self.title, + }) + + if self.breadcrumb then + local header = self.root:Object { + flex = { + flex_direction = "column", + flex_wrap = "wrap", + justify_content = "flex-start", + align_items = "flex-start", + align_content = "flex-start", + }, + w = lvgl.HOR_RES(), + h = lvgl.SIZE_CONTENT, + pad_left = 4, + pad_right = 4, + pad_bottom = 2, + bg_opa = lvgl.OPA(100), + scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, + } theme.set_style(header, "header") - header:Label { - text = opts.breadcrumb, - text_font = font.fusion_10, - } + header:Label { + text = self.breadcrumb, + text_font = font.fusion_10, + } - local buttons = header:Object({ - flex = { - flex_direction = "row", - flex_wrap = "wrap", - justify_content = "flex-end", - align_items = "center", - align_content = "center", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - pad_column = 4, - }) - local original_iterator = opts.iterator:clone() - local enqueue = widgets.IconBtn(buttons, "//lua/img/enqueue.png", "Enqueue") - enqueue:onClicked(function() - queue.add(original_iterator) - playback.playing:set(true) - end) - -- enqueue:add_flag(lvgl.FLAG.HIDDEN) - local play = widgets.IconBtn(buttons, "//lua/img/play_small.png", "Play") - play:onClicked(function() - queue.clear() - queue.add(original_iterator) - playback.playing:set(true) - backstack.push(playing) + local buttons = header:Object({ + flex = { + flex_direction = "row", + flex_wrap = "wrap", + justify_content = "flex-end", + align_items = "center", + align_content = "center", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + pad_column = 4, + }) + local original_iterator = self.iterator:clone() + local enqueue = widgets.IconBtn(buttons, "//lua/img/enqueue.png", "Enqueue") + enqueue:onClicked(function() + queue.add(original_iterator) + playback.playing:set(true) + end) + -- enqueue:add_flag(lvgl.FLAG.HIDDEN) + local play = widgets.IconBtn(buttons, "//lua/img/play_small.png", "Play") + play:onClicked(function() + queue.clear() + queue.add(original_iterator) + playback.playing:set(true) + backstack.push(playing) + end + ) end - ) - end - screen.list = lvgl.List(screen.root, { - w = lvgl.PCT(100), - h = lvgl.PCT(100), - flex_grow = 1, - scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, - }) + self.list = lvgl.List(self.root, { + w = lvgl.PCT(100), + h = lvgl.PCT(100), + flex_grow = 1, + scrollbar_mode = lvgl.SCROLLBAR_MODE.OFF, + }) - local back = screen.list:add_btn(nil, "< Back") - back:onClicked(backstack.pop) - back:add_style(styles.list_item) + local back = self.list:add_btn(nil, "< Back") + back:onClicked(backstack.pop) + back:add_style(styles.list_item) - screen.focused_item = 0 - screen.last_item = 0 - screen.add_item = function(item) - if not item then return end - screen.last_item = screen.last_item + 1 - local this_item = screen.last_item - local btn = screen.list:add_btn(nil, tostring(item)) - btn:onClicked(function() - local contents = item:contents() - if type(contents) == "userdata" then - backstack.push(function() - return browser.create({ - title = opts.title, + self.focused_item = 0 + self.last_item = 0 + self.add_item = function(item) + if not item then return end + self.last_item = self.last_item + 1 + local this_item = self.last_item + local btn = self.list:add_btn(nil, tostring(item)) + btn:onClicked(function() + local contents = item:contents() + if type(contents) == "userdata" then + backstack.push(require("browser"):new { + title = self.title, iterator = contents, breadcrumb = tostring(item), }) - end) - else - queue.clear() - queue.add(contents) - playback.playing:set(true) - backstack.push(playing) - end - end) - btn:onevent(lvgl.EVENT.FOCUSED, function() - screen.focused_item = this_item - if screen.last_item - 5 < this_item then - screen.add_item(opts.iterator()) - end - end) - btn:add_style(styles.list_item) - end + else + queue.clear() + queue.add(contents) + playback.playing:set(true) + backstack.push(playing:new()) + end + end) + btn:onevent(lvgl.EVENT.FOCUSED, function() + self.focused_item = this_item + if self.last_item - 5 < this_item then + self.add_item(self.iterator()) + end + end) + btn:add_style(styles.list_item) + end - for _ = 1, 8 do - local val = opts.iterator() - if not val then break end - screen.add_item(val) + for _ = 1, 8 do + local val = self.iterator() + if not val then break end + self.add_item(val) + end end - - return screen -end - -return browser.create +} diff --git a/lua/licenses.lua b/lua/licenses.lua index b5d1ae88..1fa392cf 100644 --- a/lua/licenses.lua +++ b/lua/licenses.lua @@ -2,20 +2,23 @@ local backstack = require("backstack") local widgets = require("widgets") local font = require("font") local styles = require("styles") +local screen = require("screen") local function show_license(text) - backstack.push(function() - local screen = widgets.MenuScreen { - show_back = true, - title = "Licenses", - } - screen.root:Label { - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - text_font = font.fusion_10, - text = text, - } - end) + backstack.push(screen:new { + createUi = function(self) + self.menu = widgets.MenuScreen { + show_back = true, + title = "Licenses", + } + self.menu.root:Label { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + text_font = font.fusion_10, + text = text, + } + end + }) end local function gpl(copyright) @@ -175,4 +178,6 @@ return function() library("tremor", "bsd", function() xiphbsd("Copyright (c) 2002, Xiph.org Foundation") end) + + return menu end diff --git a/lua/main.lua b/lua/main.lua index 291f524e..d84e2417 100644 --- a/lua/main.lua +++ b/lua/main.lua @@ -33,6 +33,18 @@ GLOBAL_BINDINGS = { container:center() end) end), + -- When the device has been locked for a while, default to showing the now + -- playing screen after unlocking. + controls.lock_switch:bind(function(locked) + if locked then + lock_time = time.ticks() + elseif time.ticks() - lock_time > 8000 then + local queue = require("queue") + if queue.size:get() > 0 then + require("playing"):pushIfNotShown() + end + end + end), } local theme_dark = require("theme_dark") diff --git a/lua/main_menu.lua b/lua/main_menu.lua index 1a9d9975..ac9190be 100644 --- a/lua/main_menu.lua +++ b/lua/main_menu.lua @@ -5,41 +5,42 @@ local backstack = require("backstack") local browser = require("browser") local playing = require("playing") local styles = require("styles") +local screen = require("screen") -return function() - local menu = widgets.MenuScreen({}) +return screen:new { + createUi = function() + local menu = widgets.MenuScreen({}) - menu.list = lvgl.List(menu.root, { - w = lvgl.PCT(100), - h = lvgl.PCT(100), - flex_grow = 1, - }) + menu.list = lvgl.List(menu.root, { + w = lvgl.PCT(100), + h = lvgl.PCT(100), + flex_grow = 1, + }) - local now_playing = menu.list:add_btn(nil, "Now Playing") - now_playing:onClicked(function() - backstack.push(playing) - end) - now_playing:add_style(styles.list_item) + local now_playing = menu.list:add_btn(nil, "Now Playing") + now_playing:onClicked(function() + backstack.push(playing:new()) + end) + now_playing:add_style(styles.list_item) - local indexes = database.indexes() - for _, idx in ipairs(indexes) do - local btn = menu.list:add_btn(nil, tostring(idx)) - btn:onClicked(function() - backstack.push(function() - return browser { + local indexes = database.indexes() + for _, idx in ipairs(indexes) do + local btn = menu.list:add_btn(nil, tostring(idx)) + btn:onClicked(function() + backstack.push(browser:new { title = tostring(idx), - iterator = idx:iter() - } + iterator = idx:iter(), + }) end) - end) - btn:add_style(styles.list_item) - end + btn:add_style(styles.list_item) + end - local settings = menu.list:add_btn(nil, "Settings") - settings:onClicked(function() - backstack.push(require("settings").root) - end) - settings:add_style(styles.list_item) + local settings = menu.list:add_btn(nil, "Settings") + settings:onClicked(function() + backstack.push(require("settings"):new()) + end) + settings:add_style(styles.list_item) - return menu -end + return menu + end, +} diff --git a/lua/playing.lua b/lua/playing.lua index 4767e42f..947bdec9 100644 --- a/lua/playing.lua +++ b/lua/playing.lua @@ -4,6 +4,7 @@ local backstack = require("backstack") local font = require("font") local playback = require("playback") local queue = require("queue") +local screen = require("screen") local img = { play = "//lua/img/play.png", @@ -18,219 +19,227 @@ local img = { repeat_disabled = "//lua/img/repeat_disabled.png", } -return function(opts) - local screen = {} - screen.root = lvgl.Object(nil, { - flex = { - flex_direction = "column", - flex_wrap = "wrap", - justify_content = "center", - align_items = "center", - align_content = "center", - }, - w = lvgl.HOR_RES(), - h = lvgl.VER_RES(), - }) - screen.root:center() - - screen.status_bar = widgets.StatusBar(screen.root, { - back_cb = backstack.pop, - transparent_bg = true, - }) - - local info = screen.root:Object { - flex = { - flex_direction = "column", - flex_wrap = "wrap", - justify_content = "center", - align_items = "center", - align_content = "center", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - flex_grow = 1, - } - - local artist = info:Label { - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - text = "", - text_font = font.fusion_10, - text_align = 2, - } - - local title = info:Label { - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - text = "", - text_align = 2, - } - - local playlist = screen.root:Object { - flex = { - flex_direction = "row", - justify_content = "center", - align_items = "center", - align_content = "center", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - } - - playlist:Object({ w = 3, h = 1 }) -- spacer - - local cur_time = playlist:Label { - w = lvgl.SIZE_CONTENT, - h = lvgl.SIZE_CONTENT, - text = "", - text_font = font.fusion_10, - } - - playlist:Object({ flex_grow = 1, h = 1 }) -- spacer - - local playlist_pos = playlist:Label { - text = "", - text_font = font.fusion_10, - } - playlist:Label { - text = "/", - text_font = font.fusion_10, - } - local playlist_total = playlist:Label { - text = "", - text_font = font.fusion_10, - } - - playlist:Object({ flex_grow = 1, h = 1 }) -- spacer - - local end_time = playlist:Label { - w = lvgl.SIZE_CONTENT, - h = lvgl.SIZE_CONTENT, - align = lvgl.ALIGN.RIGHT_MID, - text = "", - text_font = font.fusion_10, - } - playlist:Object({ w = 3, h = 1 }) -- spacer - - local scrubber = screen.root:Slider { - w = lvgl.PCT(100), - h = 5, - range = { min = 0, max = 100 }, - value = 0, - } - - scrubber:onevent(lvgl.EVENT.RELEASED, function() - playback.position:set(scrubber:value()) - end) - - local controls = screen.root:Object { - flex = { - flex_direction = "row", - justify_content = "center", - align_items = "center", - align_content = "center", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - pad_column = 8, - pad_all = 2, - } - - - controls:Object({ flex_grow = 1, h = 1 }) -- spacer - - local repeat_btn = controls:Button {} - repeat_btn:onClicked(function() - queue.repeat_track:set(not queue.repeat_track:get()) - end) - local repeat_img = repeat_btn:Image { src = img.repeat_enabled } - - local prev_btn = controls:Button {} - prev_btn:onClicked(queue.previous) - local prev_img = prev_btn:Image { src = img.prev_disabled } - - local play_pause_btn = controls:Button {} - play_pause_btn:onClicked(function() - playback.playing:set(not playback.playing:get()) - end) - play_pause_btn:focus() - local play_pause_img = play_pause_btn:Image { src = img.pause } - - local next_btn = controls:Button {} - next_btn:onClicked(queue.next) - local next_img = next_btn:Image { src = img.next_disabled } - - local shuffle_btn = controls:Button {} - shuffle_btn:onClicked(function() - queue.random:set(not queue.random:get()) - end) - local shuffle_img = shuffle_btn:Image { src = img.shuffle } - - controls:Object({ flex_grow = 1, h = 1 }) -- spacer - - - local format_time = function(time) - return string.format("%d:%02d", time // 60, time % 60) +local is_now_playing_shown = false + +return screen:new { + createUi = function(self) + self.root = lvgl.Object(nil, { + flex = { + flex_direction = "column", + flex_wrap = "wrap", + justify_content = "center", + align_items = "center", + align_content = "center", + }, + w = lvgl.HOR_RES(), + h = lvgl.VER_RES(), + }) + self.root:center() + + self.status_bar = widgets.StatusBar(self.root, { + back_cb = backstack.pop, + transparent_bg = true, + }) + + local info = self.root:Object { + flex = { + flex_direction = "column", + flex_wrap = "wrap", + justify_content = "center", + align_items = "center", + align_content = "center", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + flex_grow = 1, + } + + local artist = info:Label { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + text = "", + text_font = font.fusion_10, + text_align = 2, + } + + local title = info:Label { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + text = "", + text_align = 2, + } + + local playlist = self.root:Object { + flex = { + flex_direction = "row", + justify_content = "center", + align_items = "center", + align_content = "center", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + } + + playlist:Object({ w = 3, h = 1 }) -- spacer + + local cur_time = playlist:Label { + w = lvgl.SIZE_CONTENT, + h = lvgl.SIZE_CONTENT, + text = "", + text_font = font.fusion_10, + } + + playlist:Object({ flex_grow = 1, h = 1 }) -- spacer + + local playlist_pos = playlist:Label { + text = "", + text_font = font.fusion_10, + } + playlist:Label { + text = "/", + text_font = font.fusion_10, + } + local playlist_total = playlist:Label { + text = "", + text_font = font.fusion_10, + } + + playlist:Object({ flex_grow = 1, h = 1 }) -- spacer + + local end_time = playlist:Label { + w = lvgl.SIZE_CONTENT, + h = lvgl.SIZE_CONTENT, + align = lvgl.ALIGN.RIGHT_MID, + text = "", + text_font = font.fusion_10, + } + playlist:Object({ w = 3, h = 1 }) -- spacer + + local scrubber = self.root:Slider { + w = lvgl.PCT(100), + h = 5, + range = { min = 0, max = 100 }, + value = 0, + } + + scrubber:onevent(lvgl.EVENT.RELEASED, function() + playback.position:set(scrubber:value()) + end) + + local controls = self.root:Object { + flex = { + flex_direction = "row", + justify_content = "center", + align_items = "center", + align_content = "center", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + pad_column = 8, + pad_all = 2, + } + + + controls:Object({ flex_grow = 1, h = 1 }) -- spacer + + local repeat_btn = controls:Button {} + repeat_btn:onClicked(function() + queue.repeat_track:set(not queue.repeat_track:get()) + end) + local repeat_img = repeat_btn:Image { src = img.repeat_enabled } + + local prev_btn = controls:Button {} + prev_btn:onClicked(queue.previous) + local prev_img = prev_btn:Image { src = img.prev_disabled } + + local play_pause_btn = controls:Button {} + play_pause_btn:onClicked(function() + playback.playing:set(not playback.playing:get()) + end) + play_pause_btn:focus() + local play_pause_img = play_pause_btn:Image { src = img.pause } + + local next_btn = controls:Button {} + next_btn:onClicked(queue.next) + local next_img = next_btn:Image { src = img.next_disabled } + + local shuffle_btn = controls:Button {} + shuffle_btn:onClicked(function() + queue.random:set(not queue.random:get()) + end) + local shuffle_img = shuffle_btn:Image { src = img.shuffle } + + controls:Object({ flex_grow = 1, h = 1 }) -- spacer + + + local format_time = function(time) + return string.format("%d:%02d", time // 60, time % 60) + end + + self.bindings = { + playback.playing:bind(function(playing) + if playing then + play_pause_img:set_src(img.pause) + else + play_pause_img:set_src(img.play) + end + end), + playback.position:bind(function(pos) + if not pos then return end + cur_time:set { + text = format_time(pos) + } + if not scrubber:is_dragged() then + scrubber:set { value = pos } + end + end), + playback.track:bind(function(track) + if not track then return end + end_time:set { + text = format_time(track.duration) + } + title:set { text = track.title } + artist:set { text = track.artist } + scrubber:set { + range = { min = 0, max = track.duration } + } + end), + queue.position:bind(function(pos) + if not pos then return end + playlist_pos:set { text = tostring(pos) } + + next_img:set_src( + pos < queue.size:get() and img.next or img.next_disabled + ) + prev_img:set_src( + pos > 1 and img.prev or img.prev_disabled + ) + end), + queue.random:bind(function(shuffling) + if shuffling then + shuffle_img:set_src(img.shuffle) + else + shuffle_img:set_src(img.shuffle_disabled) + end + end), + queue.repeat_track:bind(function(en) + if en then + repeat_img:set_src(img.repeat_enabled) + else + repeat_img:set_src(img.repeat_disabled) + end + end), + queue.size:bind(function(num) + if not num then return end + playlist_total:set { text = tostring(num) } + end), + } + end, + onShown = function() is_now_playing_shown = true end, + onHidden = function() is_now_playing_shown = false end, + pushIfNotShown = function(self) + if not is_now_playing_shown then + backstack.push(self:new()) + end end - - screen.bindings = { - playback.playing:bind(function(playing) - if playing then - play_pause_img:set_src(img.pause) - else - play_pause_img:set_src(img.play) - end - end), - playback.position:bind(function(pos) - if not pos then return end - cur_time:set { - text = format_time(pos) - } - if not scrubber:is_dragged() then - scrubber:set { value = pos } - end - end), - playback.track:bind(function(track) - if not track then return end - end_time:set { - text = format_time(track.duration) - } - title:set { text = track.title } - artist:set { text = track.artist } - scrubber:set { - range = { min = 0, max = track.duration } - } - end), - queue.position:bind(function(pos) - if not pos then return end - playlist_pos:set { text = tostring(pos) } - - next_img:set_src( - pos < queue.size:get() and img.next or img.next_disabled - ) - prev_img:set_src( - pos > 1 and img.prev or img.prev_disabled - ) - end), - queue.random:bind(function(shuffling) - if shuffling then - shuffle_img:set_src(img.shuffle) - else - shuffle_img:set_src(img.shuffle_disabled) - end - end), - queue.repeat_track:bind(function(en) - if en then - repeat_img:set_src(img.repeat_enabled) - else - repeat_img:set_src(img.repeat_disabled) - end - end), - queue.size:bind(function(num) - if not num then return end - playlist_total:set { text = tostring(num) } - end), - } - - return screen -end +} diff --git a/lua/settings.lua b/lua/settings.lua index aac5ce9b..c9103de3 100644 --- a/lua/settings.lua +++ b/lua/settings.lua @@ -8,8 +8,7 @@ local controls = require("controls") local bluetooth = require("bluetooth") local theme = require("theme") local database = require("database") - -local settings = {} +local screen = require("screen") local function SettingsScreen(title) local menu = widgets.MenuScreen { @@ -32,158 +31,161 @@ local function SettingsScreen(title) return menu end -function settings.bluetooth() - local menu = SettingsScreen("Bluetooth") +local BluetoothSettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Bluetooth") + + local enable_container = self.menu.content:Object { + flex = { + flex_direction = "row", + justify_content = "flex-start", + align_items = "content", + align_content = "flex-start", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + pad_bottom = 1, + } + enable_container:Label { text = "Enable", flex_grow = 1 } + local enable_sw = enable_container:Switch {} + enable_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function() + local enabled = enable_sw:enabled() + bluetooth.enabled:set(enabled) + end) - local enable_container = menu.content:Object { - flex = { - flex_direction = "row", - justify_content = "flex-start", - align_items = "content", - align_content = "flex-start", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - pad_bottom = 1, - } - enable_container:Label { text = "Enable", flex_grow = 1 } - local enable_sw = enable_container:Switch {} - enable_sw:onevent(lvgl.EVENT.VALUE_CHANGED, function() - local enabled = enable_sw:enabled() - bluetooth.enabled:set(enabled) - end) - - theme.set_style(menu.content:Label { + theme.set_style(self.menu.content:Label { text = "Paired Device", pad_bottom = 1, }, "settings_title") - local paired_container = menu.content:Object { - flex = { - flex_direction = "row", - justify_content = "flex-start", - align_items = "flex-start", - align_content = "flex-start", - }, - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - pad_bottom = 2, - } + local paired_container = self.menu.content:Object { + flex = { + flex_direction = "row", + justify_content = "flex-start", + align_items = "flex-start", + align_content = "flex-start", + }, + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + pad_bottom = 2, + } - local paired_device = paired_container:Label { - flex_grow = 1, - } - local clear_paired = paired_container:Button {} - clear_paired:Label { text = "x" } - clear_paired:onClicked(function() - bluetooth.paired_device:set() - end) + local paired_device = paired_container:Label { + flex_grow = 1, + } + local clear_paired = paired_container:Button {} + clear_paired:Label { text = "x" } + clear_paired:onClicked(function() + bluetooth.paired_device:set() + end) - theme.set_style(menu.content:Label { + theme.set_style(self.menu.content:Label { text = "Nearby Devices", pad_bottom = 1, }, "settings_title") - local devices = menu.content:List { - w = lvgl.PCT(100), - h = lvgl.SIZE_CONTENT, - } + local devices = self.menu.content:List { + w = lvgl.PCT(100), + h = lvgl.SIZE_CONTENT, + } - menu.bindings = { - bluetooth.enabled:bind(function(en) - if en then - enable_sw:add_state(lvgl.STATE.CHECKED) - else - enable_sw:clear_state(lvgl.STATE.CHECKED) - end - end), - bluetooth.paired_device:bind(function(device) - if device then - paired_device:set { text = device.name } - clear_paired:clear_flag(lvgl.FLAG.HIDDEN) - else - paired_device:set { text = "None" } - clear_paired:add_flag(lvgl.FLAG.HIDDEN) - end - end), - bluetooth.devices:bind(function(devs) - devices:clean() - for _, dev in pairs(devs) do - devices:add_btn(nil, dev.name):onClicked(function() - bluetooth.paired_device:set(dev) - end) - end - end) - } -end + self.bindings = { + bluetooth.enabled:bind(function(en) + if en then + enable_sw:add_state(lvgl.STATE.CHECKED) + else + enable_sw:clear_state(lvgl.STATE.CHECKED) + end + end), + bluetooth.paired_device:bind(function(device) + if device then + paired_device:set { text = device.name } + clear_paired:clear_flag(lvgl.FLAG.HIDDEN) + else + paired_device:set { text = "None" } + clear_paired:add_flag(lvgl.FLAG.HIDDEN) + end + end), + bluetooth.devices:bind(function(devs) + devices:clean() + for _, dev in pairs(devs) do + devices:add_btn(nil, dev.name):onClicked(function() + bluetooth.paired_device:set(dev) + end) + end + end) + } + end +} -function settings.headphones() - local menu = SettingsScreen("Headphones") +local HeadphonesSettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Headphones") - theme.set_style(menu.content:Label { + theme.set_style(self.menu.content:Label { text = "Maxiumum volume limit", }, "settings_title") - local volume_chooser = menu.content:Dropdown { - options = "Line Level (-10 dB)\nCD Level (+6 dB)\nMaximum (+10dB)", - selected = 1, - } - local limits = { -10, 6, 10 } - volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() - -- luavgl dropdown binding uses 0-based indexing :( - local selection = volume_chooser:get('selected') + 1 - volume.limit_db:set(limits[selection]) - end) - - theme.set_style(menu.content:Label { + local volume_chooser = self.menu.content:Dropdown { + options = "Line Level (-10 dB)\nCD Level (+6 dB)\nMaximum (+10dB)", + selected = 1, + } + local limits = { -10, 6, 10 } + volume_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() + -- luavgl dropdown binding uses 0-based indexing :( + local selection = volume_chooser:get('selected') + 1 + volume.limit_db:set(limits[selection]) + end) + + theme.set_style(self.menu.content:Label { text = "Left/Right balance", }, "settings_title") - local balance = menu.content:Slider { - w = lvgl.PCT(100), - h = 5, - range = { min = -100, max = 100 }, - value = 0, - } - balance:onevent(lvgl.EVENT.VALUE_CHANGED, function() - volume.left_bias:set(balance:value()) - end) + local balance = self.menu.content:Slider { + w = lvgl.PCT(100), + h = 5, + range = { min = -100, max = 100 }, + value = 0, + } + balance:onevent(lvgl.EVENT.VALUE_CHANGED, function() + volume.left_bias:set(balance:value()) + end) - local balance_label = menu.content:Label {} + local balance_label = self.menu.content:Label {} - menu.bindings = { - volume.limit_db:bind(function(limit) - for i = 1, #limits do - if limits[i] == limit then - volume_chooser:set { selected = i - 1 } + self.bindings = { + volume.limit_db:bind(function(limit) + for i = 1, #limits do + if limits[i] == limit then + volume_chooser:set { selected = i - 1 } + end end - end - end), - volume.left_bias:bind(function(bias) - balance:set { - value = bias - } - if bias < 0 then - balance_label:set { - text = string.format("Left %.2fdB", bias / 4) + end), + volume.left_bias:bind(function(bias) + balance:set { + value = bias } - elseif bias > 0 then - balance_label:set { - text = string.format("Right %.2fdB", -bias / 4) - } - else - balance_label:set { text = "Balanced" } - end - end), - } - - return menu -end + if bias < 0 then + balance_label:set { + text = string.format("Left %.2fdB", bias / 4) + } + elseif bias > 0 then + balance_label:set { + text = string.format("Right %.2fdB", -bias / 4) + } + else + balance_label:set { text = "Balanced" } + end + end), + } + end +} -function settings.display() - local menu = SettingsScreen("Display") +local DisplaySettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Display") - local brightness_title = menu.content:Object { + local brightness_title = self.menu.content:Object { flex = { flex_direction = "row", justify_content = "flex-start", @@ -198,91 +200,91 @@ function settings.display() local brightness_pct = brightness_title:Label {} theme.set_style(brightness_pct, "settings_title") - local brightness = menu.content:Slider { - w = lvgl.PCT(100), - h = 5, - range = { min = 0, max = 100 }, - value = display.brightness:get(), - } - brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function() - display.brightness:set(brightness:value()) - end) - - menu.bindings = { - display.brightness:bind(function(b) - brightness_pct:set { text = tostring(b) .. "%" } + local brightness = self.menu.content:Slider { + w = lvgl.PCT(100), + h = 5, + range = { min = 0, max = 100 }, + value = display.brightness:get(), + } + brightness:onevent(lvgl.EVENT.VALUE_CHANGED, function() + display.brightness:set(brightness:value()) end) - } - return menu -end + self.bindings = { + display.brightness:bind(function(b) + brightness_pct:set { text = tostring(b) .. "%" } + end) + } + end +} -function settings.input() - local menu = SettingsScreen("Input Method") +local InputSettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Input Method") - theme.set_style(menu.content:Label { + theme.set_style(self.menu.content:Label { text = "Control scheme", }, "settings_title") - local schemes = controls.schemes() - local option_to_scheme = {} - local scheme_to_option = {} + local schemes = controls.schemes() + local option_to_scheme = {} + local scheme_to_option = {} - local option_idx = 0 - local options = "" + local option_idx = 0 + local options = "" - for i, v in pairs(schemes) do - option_to_scheme[option_idx] = i - scheme_to_option[i] = option_idx - if option_idx > 0 then - options = options .. "\n" + for i, v in pairs(schemes) do + option_to_scheme[option_idx] = i + scheme_to_option[i] = option_idx + if option_idx > 0 then + options = options .. "\n" + end + options = options .. v + option_idx = option_idx + 1 end - options = options .. v - option_idx = option_idx + 1 - end - local controls_chooser = menu.content:Dropdown { - options = options, - } + local controls_chooser = self.menu.content:Dropdown { + options = options, + } - menu.bindings = { - controls.scheme:bind(function(s) - local option = scheme_to_option[s] - controls_chooser:set({ selected = option }) - end) - } + self.bindings = { + controls.scheme:bind(function(s) + local option = scheme_to_option[s] + controls_chooser:set({ selected = option }) + end) + } - controls_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() - local option = controls_chooser:get('selected') - local scheme = option_to_scheme[option] - controls.scheme:set(scheme) - end) + controls_chooser:onevent(lvgl.EVENT.VALUE_CHANGED, function() + local option = controls_chooser:get('selected') + local scheme = option_to_scheme[option] + controls.scheme:set(scheme) + end) - theme.set_style(menu.content:Label { + theme.set_style(self.menu.content:Label { text = "Scroll Sensitivity", }, "settings_title") - local slider_scale = 4; -- Power steering - local sensitivity = menu.content:Slider { - w = lvgl.PCT(90), - h = 5, - range = { min = 0, max = 255/slider_scale }, - value = controls.scroll_sensitivity:get()/slider_scale, - } - sensitivity:onevent(lvgl.EVENT.VALUE_CHANGED, function() - controls.scroll_sensitivity:set(sensitivity:value()*slider_scale) - end) - - return menu -end + local slider_scale = 4; -- Power steering + local sensitivity = self.menu.content:Slider { + w = lvgl.PCT(90), + h = 5, + range = { min = 0, max = 255 / slider_scale }, + value = controls.scroll_sensitivity:get() / slider_scale, + } + sensitivity:onevent(lvgl.EVENT.VALUE_CHANGED, function() + controls.scroll_sensitivity:set(sensitivity:value() * slider_scale) + end) + end +} -function settings.database() - local menu = SettingsScreen("Database") - local db = require("database") - widgets.Row(menu.content, "Schema version", db.version()) - widgets.Row(menu.content, "Size on disk", string.format("%.1f KiB", db.size() / 1024)) +local DatabaseSettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Database") + local db = require("database") + widgets.Row(self.menu.content, "Schema version", db.version()) + widgets.Row(self.menu.content, "Size on disk", string.format("%.1f KiB", db.size() / 1024)) - local actions_container = menu.content:Object { + local actions_container = self.menu.content:Object { w = lvgl.PCT(100), h = lvgl.SIZE_CONTENT, flex = { @@ -296,64 +298,69 @@ function settings.database() } actions_container:add_style(styles.list_item) - local update = actions_container:Button {} - update:Label { text = "Update" } - update:onClicked(function() - database.update() - end) -end + local update = actions_container:Button {} + update:Label { text = "Update" } + update:onClicked(function() + database.update() + end) + end +} + +local FirmwareSettings = screen:new { + createUi = function(self) + self.menu = SettingsScreen("Firmware") + local version = require("version") + widgets.Row(self.menu.content, "ESP32", version.esp()) + widgets.Row(self.menu.content, "SAMD21", version.samd()) + widgets.Row(self.menu.content, "Collator", version.collator()) + end +} -function settings.firmware() - local menu = SettingsScreen("Firmware") - local version = require("version") - widgets.Row(menu.content, "ESP32", version.esp()) - widgets.Row(menu.content, "SAMD21", version.samd()) - widgets.Row(menu.content, "Collator", version.collator()) -end +local LicensesScreen = screen:new { + createUi = function(self) + self.root = require("licenses")() + end +} -function settings.root() - local menu = widgets.MenuScreen { - show_back = true, - title = "Settings", - } - menu.list = menu.root:List { - w = lvgl.PCT(100), - h = lvgl.PCT(100), - flex_grow = 1, - } +return screen:new { + createUi = function(self) + self.menu = widgets.MenuScreen { + show_back = true, + title = "Settings", + } + self.list = self.menu.root:List { + w = lvgl.PCT(100), + h = lvgl.PCT(100), + flex_grow = 1, + } local function section(name) - local elem = menu.list:Label { + local elem = self.list:Label { text = name, pad_left = 4, } theme.set_style(elem, "settings_title") end - local function submenu(name, fn) - local item = menu.list:add_btn(nil, name) - item:onClicked(function() - backstack.push(fn) - end) - item:add_style(styles.list_item) - end - - section("Audio") - submenu("Bluetooth", settings.bluetooth) - submenu("Headphones", settings.headphones) - - section("Interface") - submenu("Display", settings.display) - submenu("Input Method", settings.input) + local function submenu(name, class) + local item = self.list:add_btn(nil, name) + item:onClicked(function() + backstack.push(class:new()) + end) + item:add_style(styles.list_item) + end - section("System") - submenu("Database", settings.database) - submenu("Firmware", settings.firmware) - submenu("Licenses", function() - return require("licenses")() - end) + section("Audio") + submenu("Bluetooth", BluetoothSettings) + submenu("Headphones", HeadphonesSettings) - return menu -end + section("Interface") + submenu("Display", DisplaySettings) + submenu("Input Method", InputSettings) -return settings + section("System") + submenu("Database", DatabaseSettings) + submenu("Firmware", FirmwareSettings) + submenu("Licenses", LicensesScreen) + end +} diff --git a/luals-stubs/alerts.lua b/luals-stubs/alerts.lua index d430f12d..420194eb 100644 --- a/luals-stubs/alerts.lua +++ b/luals-stubs/alerts.lua @@ -1,11 +1,15 @@ --- @meta +--- The `alerts` module contains functions for showing transient popups over +--- the current screen. --- @class 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 +--- Dismisses any visible alerts, removing them from the screen. function alerts.hide() end return alerts diff --git a/luals-stubs/backstack.lua b/luals-stubs/backstack.lua index 2e4eccb3..b39fcbf2 100644 --- a/luals-stubs/backstack.lua +++ b/luals-stubs/backstack.lua @@ -1,11 +1,20 @@ --- @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 local backstack = {} ---- @param constructor function -function backstack.push(constructor) end +--- Displays the given screen to the user. If there was already a screen being +--- 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 return backstack diff --git a/luals-stubs/bluetooth.lua b/luals-stubs/bluetooth.lua index 09fc7606..a2dd476d 100644 --- a/luals-stubs/bluetooth.lua +++ b/luals-stubs/bluetooth.lua @@ -1,8 +1,12 @@ --- @meta +--- The 'bluetooth' module contains Properties and functions for interacting +--- with the device's Bluetooth capabilities. --- @class bluetooth ---- @field enabled Property ---- @field connected 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 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 = {} return bluetooth diff --git a/luals-stubs/controls.lua b/luals-stubs/controls.lua new file mode 100644 index 00000000..7034dc55 --- /dev/null +++ b/luals-stubs/controls.lua @@ -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 diff --git a/luals-stubs/database.lua b/luals-stubs/database.lua index e23c085b..753961fe 100644 --- a/luals-stubs/database.lua +++ b/luals-stubs/database.lua @@ -1,33 +1,60 @@ --- @meta +--- The `database` module contains Properties and functions for working with +--- the device's LevelDB-backed track database. --- @class database +--- @field updating Property Whether or not a database re-index is currently in progress. local database = {} +--- Returns a list of all indexes in the database. --- @return Index[] function database.indexes() end +--- An iterator is a userdata type that behaves like an ordinary Lua iterator. --- @class Iterator local Iterator = {} +--- A TrackId is a unique identifier, representing a playable track in the +--- user's library. --- @class TrackId local TrackId = {} +--- Gets the human-readable text representing this record. The `__tostring` +--- metatable function is an alias of this function. --- @class Record local Record = {} --- @return string 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 +--- 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 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 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 return database diff --git a/luals-stubs/display.lua b/luals-stubs/display.lua new file mode 100644 index 00000000..74bcdca9 --- /dev/null +++ b/luals-stubs/display.lua @@ -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 diff --git a/luals-stubs/lvgl.lua b/luals-stubs/lvgl.lua new file mode 100644 index 00000000..59093000 --- /dev/null +++ b/luals-stubs/lvgl.lua @@ -0,0 +1,1546 @@ +---@meta +--- +--- lvgl comments +--- + +--- The `lvgl` module provides bindings to the LVGL graphics library. These +--- bindings were originally based on [luavgl](https://github.com/XuNeo/luavgl/), +--- but have diverged somewhat. These bindings are also largely a very thin +--- abstraction around LVGL's C API, so [LVGL's documentation](https://docs.lvgl.io/8.3/) +--- may also be useful to reference. +--- This module is currently only available from the UI Lua context; i.e. the +--- `main.lua` script run after boot on-device. +---@class lvgl +lvgl = {} + +--- Event codes for use with `obj:onevent`. See the [LVGL docs](https://docs.lvgl.io/8.3/overview/event.html#event-codes) +--- for a description of each event type. +--- @enum ObjEventCode +lvgl.EVENT = { + ALL = 0, + PRESSED = 0, + PRESSING = 0, + PRESS_LOST = 0, + SHORT_CLICKED = 0, + LONG_PRESSED = 0, + LONG_PRESSED_REPEAT = 0, + CLICKED = 0, + RELEASED = 0, + SCROLL_BEGIN = 0, + SCROLL_END = 0, + SCROLL = 0, + GESTURE = 0, + KEY = 0, + FOCUSED = 0, + DEFOCUSED = 0, + LEAVE = 0, + HIT_TEST = 0, + COVER_CHECK = 0, + REFR_EXT_DRAW_SIZE = 0, + DRAW_MAIN_BEGIN = 0, + DRAW_MAIN = 0, + DRAW_MAIN_END = 0, + DRAW_POST_BEGIN = 0, + DRAW_POST = 0, + DRAW_POST_END = 0, + DRAW_PART_BEGIN = 0, + DRAW_PART_END = 0, + VALUE_CHANGED = 0, + INSERT = 0, + REFRESH = 0, + READY = 0, + CANCEL = 0, + DELETE = 0, + CHILD_CHANGED = 0, + CHILD_CREATED = 0, + CHILD_DELETED = 0, + SCREEN_UNLOAD_START = 0, + SCREEN_LOAD_START = 0, + SCREEN_LOADED = 0, + SCREEN_UNLOADED = 0, + SIZE_CHANGED = 0, + STYLE_CHANGED = 0, + LAYOUT_CHANGED = 0, + GET_SELF_SIZE = 0, +} + +--- Flags that can be set for each LVGL object. +--- @enum ObjFlag +lvgl.FLAG = { + PRESSED = 0, + HIDDEN = 0, + CLICKABLE = 0, + CLICK_FOCUSABLE = 0, + CHECKABLE = 0, + SCROLLABLE = 0, + SCROLL_ELASTIC = 0, + SCROLL_MOMENTUM = 0, + SCROLL_ONE = 0, + SCROLL_CHAIN_HOR = 0, + SCROLL_CHAIN_VER = 0, + SCROLL_CHAIN = 0, + SCROLL_ON_FOCUS = 0, + SCROLL_WITH_ARROW = 0, + SNAPPABLE = 0, + PRESS_LOCK = 0, + EVENT_BUBBLE = 0, + GESTURE_BUBBLE = 0, + ADV_HITTEST = 0, + IGNORE_LAYOUT = 0, + FLOATING = 0, + OVERFLOW_VISIBLE = 0, + LAYOUT_1 = 0, + LAYOUT_2 = 0, + WIDGET_1 = 0, + WIDGET_2 = 0, + USER_1 = 0, + USER_2 = 0, + USER_3 = 0, + USER_4 = 0, +} + +--- States that can be set on each LVGL object. See the [LVGL docs](https://docs.lvgl.io/8.3/overview/object.html#states) +--- for an explanation of what each state means. +--- @enum ObjState +lvgl.STATE = { + DEFAULT = 0, + CHECKED = 0, + FOCUSED = 0, + FOCUS_KEY = 0, + EDITED = 0, + HOVERED = 0, + PRESSED = 0, + SCROLLED = 0, + DISABLED = 0, + USER_1 = 0, + USER_2 = 0, + USER_3 = 0, + USER_4 = 0, + ANY = 0, +} + +--- Enum for specifying what kind of alignment to use for an LVGL object. See +--- the [LVGL docs](https://docs.lvgl.io/8.3/overview/style-props.html#align) +--- for an explanation of each value. +--- @enum ObjAlignType +lvgl.ALIGN = { + DEFAULT = 0, + TOP_LEFT = 0, + TOP_MID = 0, + TOP_RIGHT = 0, + BOTTOM_LEFT = 0, + BOTTOM_MID = 0, + BOTTOM_RIGHT = 0, + LEFT_MID = 0, + RIGHT_MID = 0, + CENTER = 0, + OUT_TOP_LEFT = 0, + OUT_TOP_MID = 0, + OUT_TOP_RIGHT = 0, + OUT_BOTTOM_LEFT = 0, + OUT_BOTTOM_MID = 0, + OUT_BOTTOM_RIGHT = 0, + OUT_LEFT_TOP = 0, + OUT_LEFT_MID = 0, + OUT_LEFT_BOTTOM = 0, + OUT_RIGHT_TOP = 0, + OUT_RIGHT_MID = 0, + OUT_RIGHT_BOTTOM = 0, +} + +--- Long modes for use with labels. +--- @enum LABEL +lvgl.LABEL = { + LONG_WRAP = 0, + LONG_DOT = 0, + LONG_SCROLL = 0, + LONG_SCROLL_CIRCULAR = 0, + LONG_CLIP = 0, +} + +--- Scroll modes +--- @enum ScrollbarMode +lvgl.SCROLLBAR_MODE = { + OFF = 0, + ON = 0, + ACTIVE = 0, + AUTO = 0, +} + +--- Directions values for dropdown and scroll directions +--- @enum Dir +lvgl.DIR = { + NONE = 0, + LEFT = 0, + RIGHT = 0, + TOP = 0, + BOTTOM = 0, + HOR = 0, + VER = 0, + ALL = 0, +} + +--- Keyboard modes +--- @enum KeyboardMode +lvgl.KEYBOARD_MODE = { + TEXT_LOWER = 0, + TEXT_UPPER = 0, + SPECIAL = 0, + NUMBER = 0, + USER_1 = 0, + USER_2 = 0, + USER_3 = 0, + USER_4 = 0, + TEXT_ARABIC = 0, +} + +--- Flow direction for flex layouts. See the [LVGL docs](https://docs.lvgl.io/8.3/layouts/flex.html#flex-flow) +--- for more details. +--- @enum FlexFlow +lvgl.FLEX_FLOW = { + ROW = 0, + COLUMN = 0, + ROW_WRAP = 0, + ROW_REVERSE = 0, + ROW_WRAP_REVERSE = 0, + COLUMN_WRAP = 0, + COLUMN_REVERSE = 0, + COLUMN_WRAP_REVERSE = 0, +} + +--- Alignment values for flex layouts. See the [LVGL docs](https://docs.lvgl.io/8.3/layouts/flex.html#flex-align) +--- for more details. +--- @enum FlexAlign +lvgl.FLEX_ALIGN = { + START = 0, + END = 0, + CENTER = 0, + SPACE_EVENLY = 0, + SPACE_AROUND = 0, + SPACE_BETWEEN = 0, +} + +--- Alignment values for grid layouts. See the [LVGL docs](https://docs.lvgl.io/8.3/layouts/grid.html#grid-align) +--- for more details. +--- @enum GridAlign +lvgl.GRID_ALIGN = { + START = 0, + CENTER = 0, + END = 0, + STRETCH = 0, + SPACE_EVENLY = 0, + SPACE_AROUND = 0, + SPACE_BETWEEN = 0, +} + +--- Values for KEY events. +--- @enum KEY +lvgl.KEY = { + UP = 0, + DOWN = 0, + RIGHT = 0, + LEFT = 0, + ESC = 0, + DEL = 0, + BACKSPACE = 0, + ENTER = 0, + NEXT = 0, + PREV = 0, + HOME = 0, + END = 0, +} + +lvgl.ANIM_REPEAT_INFINITE = 0 +lvgl.ANIM_PLAYTIME_INFINITE = 0 +lvgl.SIZE_CONTENT = 0 +lvgl.RADIUS_CIRCLE = 0 +lvgl.COORD_MAX = 0 +lvgl.COORD_MIN = 0 +lvgl.IMG_ZOOM_NONE = 0 +lvgl.BTNMATRIX_BTN_NONE = 0 +lvgl.CHART_POINT_NONE = 0 +lvgl.DROPDOWN_POS_LAST = 0 +lvgl.LABEL_DOT_NUM = 0 +lvgl.LABEL_POS_LAST = 0 +lvgl.LABEL_TEXT_SELECTION_OFF = 0 +lvgl.TABLE_CELL_NONE = 0 +lvgl.TEXTAREA_CURSOR_LAST = 0 +lvgl.LAYOUT_FLEX = 0 +lvgl.LAYOUT_GRID = 0 + +--- Converts an opacity value as a percentage into the LVGL opacity range of 0 +--- to 255. +---@param p integer opacity value in range of 0..100 +---@return integer opacity value in the range of 0..255 +function lvgl.OPA(p) +end + +--- Converts a size in percent into an LVGL size value. +---@param p integer size percentage +---@return integer size in LVGL units +function lvgl.PCT(p) +end + +--- Returns the horizontal resolution of the display. +---@return integer +function lvgl.HOR_RES() +end + +--- Returns the vertical resolution of the display. +---@return integer +function lvgl.VER_RES() +end + +--- Creates a new base LVGL object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/obj.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? StyleProp Style properties to apply to this object +--- @return Object +function lvgl.Object(parent, property) +end + +--- Create a new Bar widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/bar.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? BarStyle Style properties to apply to this object +--- @return Bar +function lvgl.Bar(parent, property) +end + +--- Create a new Button widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/btn.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? StyleProp Style properties to apply to this object +--- @return Button +function lvgl.Button(parent, property) +end + +--- Create a new Calendar widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/extra/calendar.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? CalendarStyle Style properties to apply to this object +--- @return Calendar +function lvgl.Calendar(parent, property) +end + +--- Create a new Checkbox widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/checkbox.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? CheckboxStyle Style properties to apply to this object +--- @return Checkbox +function lvgl.Checkbox(parent, property) +end + +--- Create a new Dropdown widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/dropdown.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? DropdownStyle Style properties to apply to this object +--- @return Dropdown +function lvgl.Dropdown(parent, property) +end + +--- Create a new Image widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/img.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? ImageStyle Style properties to apply to this object +--- @return Image +function lvgl.Image(parent, property) +end + +--- Create a new Label widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/label.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? LabelStyle Style properties to apply to this object +--- @return Label +function lvgl.Label(parent, property) +end + +--- Create a new Text Area widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/textarea.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? TextareaStyle Style properties to apply to this object +--- @return Textarea +function lvgl.Textarea(parent, property) +end + +--- Create a new Keyboard widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/extra/keyboard.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? KeyboardStyle Style properties to apply to this object +--- @return Keyboard +function lvgl.Keyboard(parent, property) +end + +--- Create a new LED widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/extra/led.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? LedStyle Style properties to apply to this object +--- @return Led +function lvgl.Led(parent, property) +end + +--- Create a new List widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/extra/list.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? ListStyle Style properties to apply to this object +--- @return List +function lvgl.List(parent, property) +end + +--- Create a new Roller widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/roller.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? RollerStyle Style properties to apply to this object +--- @return Roller +function lvgl.Roller(parent, property) +end + +--- Create a new Slider widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/slider.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? BarStyle Style properties to apply to this object. Sliders use the same style properties as Bars. +--- @return Slider +function lvgl.Slider(parent, property) +end + +--- Create a new Switch widget. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/switch.html) +--- for details about this widget. +--- @param parent? Object The parent for this object, or nil to add to the screen root. +--- @param property? StyleProp Style properties to apply to this object +--- @return Switch +function lvgl.Switch(parent, property) +end + +--- Create a new Timer. See the [LVGL docs](https://docs.lvgl.io/8.3/overview/timer.html) +--- for more details on LVGL's Timers system. +--- @param p TimerPara Parameters to use for configuring the timer. +--- @return Timer +function lvgl.Timer(p) +end + +--- Create a new a font. Currently only the inbuilt "fusion" font family is +--- available, in "normal" weight and sizes 12 and 10. +--- @param family string Name of the font family. +--- @param size integer the font size in px +--- @param weight string the weight of the font. e.g.g "normal", "bold", "light" +--- @return Font +function lvgl.Font(family, size, weight) +end + +--- Create a new style that can be applied to objects via `obj:add_style`. +--- @param p? StyleProp Style properties that will be applied by this style. +--- @return Style +function lvgl.Style(p) +end + +--- +--- Base LVGL object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/obj.html) +--- for additional details. +--- @class Object +obj = {} + +--- Creates a new base LVGL object as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/obj.html) +--- for details about this widget. +--- @param property? StyleProp Style properties to apply to this object +--- @return Object +function obj:Object(property) +end + +--- Create a new Bar widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/bar.html) +--- for details about this widget. +--- @param property? BarStyle Style properties to apply to this object +--- @return Bar +function obj:Bar(property) +end + +--- Create a new Button widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/btn.html) +--- for details about this widget. +--- @param property? StyleProp Style properties to apply to this object +--- @return Button +function obj:Button(property) +end + +--- Create a new Calendar widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/extra/calendar.html) +--- for details about this widget. +--- @param property? CalendarStyle Style properties to apply to this object +--- @return Calendar +function obj:Calendar(property) +end + +--- Create a new Checkbox widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/checkbox.html) +--- for details about this widget. +--- @param property? CheckboxStyle Style properties to apply to this object +--- @return Checkbox +function obj:Checkbox(property) +end + +--- Create a new Dropdown widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/dropdown.html) +--- for details about this widget. +--- @param property? DropdownStyle Style properties to apply to this object +--- @return Dropdown +function obj:Dropdown(parent, property) +end + +--- Create a new Image widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/img.html) +--- for details about this widget. +--- @param property? ImageStyle Style properties to apply to this object +--- @return Image +function obj:Image(property) +end + +--- Create a new Label widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/label.html) +--- for details about this widget. +--- @param property? LabelStyle Style properties to apply to this object +--- @return Label +function obj:Label(property) +end + +--- Create a new Text Area widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/textarea.html) +--- for details about this widget. +--- @param property? TextareaStyle Style properties to apply to this object +--- @return Textarea +function obj:Textarea(property) +end + +--- Create a new Keyboard widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/extra/keyboard.html) +--- for details about this widget. +--- @param property? KeyboardStyle Style properties to apply to this object +--- @return Keyboard +function obj:Keyboard(property) +end + +--- Create a new LED widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/extra/led.html) +--- for details about this widget. +--- @param property? LedStyle Style properties to apply to this object +--- @return Led +function obj:Led(property) +end + +--- Create a new List widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/extra/list.html) +--- for details about this widget. +--- @param property? ListStyle Style properties to apply to this object +--- @return List +function obj:List(property) +end + +--- Create a new Roller widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/roller.html) +--- for details about this widget. +--- @param property? RollerStyle Style properties to apply to this object +--- @return Roller +function obj:Roller(parent, property) +end + +--- Create a new Slider widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/slider.html) +--- for details about this widget. +--- @param property? BarStyle Style properties to apply to this object. Sliders use the same style properties as Bars. +--- @return Slider +function obj:Slider(property) +end + +--- Create a new Switch widget as a child of this object. See the [LVGL docs](https://docs.lvgl.io/8.3/widgets/core/switch.html) +--- for details about this widget. +--- @param property? StyleProp Style properties to apply to this object +--- @return Switch +function obj:Switch(property) +end + +--- Sets new style properties on this object. +--- @param p StyleProp Style properties to be applied. +function obj:set(p) +end + +--- Sets new style properties on this object. +--- @param p StyleProp Style properties to be applied. +--- @param selector integer Selector to detemine when the style is used +--- +function obj:set_style(p, selector) +end + +--- Sets this object's position relative to another object. +--- @param p AlignToPara +function obj:align_to(p) +end + +--- Deletes this object, removing it from the view hierarchy. +function obj:delete() +end + +--- Delete all children of this object +function obj:clean() +end + +--- Sets the parent of this object, detaching it from any existing parent. +--- @param p Object The new parent object +function obj:set_parent(p) +end + +--- Gets this object's parent +--- @return Object Parent +function obj:get_parent() +end + +--- +--- set and/or get object's parent +--- @param p Object +--- @return Object +function obj:parent(p) +end + +--- +--- get child object +--- @param id integer 0 the first child, -1 the lastly created child +--- @return Object +function obj:get_child(id) +end + +--- +--- get object children count +--- @return integer +function obj:get_child_cnt() +end + +--- +--- get the state of this object +--- @return ObjState +function obj:get_state(p) +end + +--- +--- Scroll to a given coordinate on an object. +--- @class ScrollToPara +--- @field x integer position x +--- @field y integer +--- @field anim boolean +--- +--- @param p ScrollToPara +--- @return ObjState +function obj:scroll_to(p) +end + +--- +--- Tell whether an object is being scrolled or not at this moment +--- @return boolean +function obj:is_scrolling() +end + +--- +--- Tell whether an object is visible (even partially) now or not +--- @return boolean +function obj:is_visible() +end + +--- +--- add flag to object +--- @param p ObjFlag +--- @return nil +function obj:add_flag(p) +end + +--- +--- clear object flag +--- @param p ObjFlag +--- @return nil +function obj:clear_flag(p) +end + +--- +--- add state to object +--- @param p ObjState +--- @return nil +function obj:add_state(p) +end + +--- +--- clear object state +--- @param p ObjState +--- @return nil +function obj:clear_state(p) +end + +--- +--- add style to object +--- @param s Style +--- @param selector? integer +--- @return nil +function obj:add_style(s, selector) +end + +--- +--- remove style from object +--- @param s Style +--- @param selector? integer +--- @return nil +function obj:remove_style(s, selector) +end + +--- +--- remove all style from object +--- @return nil +function obj:remove_style_all() +end + +---scroll obj by x,y +---@param x integer +---@param y integer +---@param anim_en? boolean +function obj:scroll_by(x, y, anim_en) +end + +---scroll obj by x,y +---@param x integer +---@param y integer +---@param anim_en boolean +function obj:scroll_by_bounded(x, y, anim_en) +end + +--- Scroll to an object until it becomes visible on its parent +---@param anim_en? boolean +function obj:scroll_to_view(anim_en) +end + +--- Scroll to an object until it becomes visible on its parent +--- Do the same on the parent's parent, and so on. +--- Therefore the object will be scrolled into view even it has nested scrollable parents +---@param anim_en? boolean +function obj:scroll_to_view_recursive(anim_en) +end + +---scroll obj by x,y, low level APIs +---@param x integer +---@param y integer +---@param anim_en boolean +function obj:scroll_by_raw(x, y, anim_en) +end + +---Invalidate the area of the scrollbars +function obj:scrollbar_invalidate() +end + +---Checked if the content is scrolled "in" and adjusts it to a normal position. +---@param anim_en boolean +function obj:readjust_scroll(anim_en) +end + +---If object is editable +---@return boolean +function obj:is_editable() +end + +--- class group def +---@return boolean +function obj:is_group_def() +end + +--- Test whether the and object is positioned by a layout or not +---@return boolean +function obj:is_layout_positioned() +end + +--- Mark the object for layout update. +---@return nil +function obj:mark_layout_as_dirty() +end + +--- Align an object to the center on its parent. same as obj:set{align={type = lvgl.ALIGN.CENTER}} +---@return nil +function obj:center() +end + +--- Align an object to the center on its parent. same as obj:set{align={type = lvgl.ALIGN.CENTER}} +---@return nil +function obj:invalidate() +end + +--- Sets this object as the current selection of the object's group. +---@return nil +function obj:focus() +end + +--- +--- Object event callback. `para` is not used for now. +--- @alias EventCallback fun(obj:Object, code: ObjEventCode): nil +--- +--- set object event callback +--- @param code ObjEventCode +--- @param cb EventCallback +--- @return nil +function obj:onevent(code, cb) +end + +--- +--- set object pressed event callback, same as obj:onevent(lvgl.EVENT.PRESSED, cb) +--- @param cb EventCallback +--- @return nil +function obj:onPressed(cb) +end + +--- +--- set object clicked event callback, same as obj:onevent(lvgl.EVENT.CLICKED, cb) +--- @param cb EventCallback +--- @return nil +function obj:onClicked(cb) +end + +--- +--- set object short clicked event callback, same as obj:onevent(lvgl.EVENT.SHORT_CLICKED, cb) +--- @param cb EventCallback +--- @return nil +function obj:onShortClicked(cb) +end + +--- +--- Create anim for object +--- @param p AnimPara +--- @return Anim +function obj:Anim(p) +end + +--- +--- Get coords of object +--- @return Coords coords +function obj:get_coords() +end + +--- +--- Get real postion of object relative to its parent +--- @return Coords coords +function obj:get_pos() +end + +--- +--- Calendar widget +---@class Calendar:Object +--- +local calendar = {} + +--- set method for calendar widget +--- @param p CalendarStyle +--- @return nil +function calendar:set(p) +end + +--- get today para setting from calendar widget +--- @return CalendarDatePara +function calendar:get_today(p) +end + +--- get the currently showed date +--- @return CalendarDatePara +function calendar:get_showed(p) +end + +--- get the currently pressed day +--- @return CalendarDatePara +function calendar:get_pressed(p) +end + +--- get the button matrix object of the calendar. +--- @return Object +function calendar:get_btnm(p) +end + +--- create a calendar header with drop-drowns to select the year and month. +--- @return Object +function calendar:Arrow(p) +end + +--- create a calendar header with drop-drowns to select the year and month +--- @return Object +function calendar:Dropdown(p) +end + +--- +--- Bar widget +---@class Bar:Object +--- +local bar = {} + +--- set method for bar widget +--- @param p BarStyle +--- @return nil +function bar:set(p) +end + +--- +--- Button widget +---@class Button:Object +--- +local button = {} + +--- set method for button widget +--- @param p ButtonStyle +--- @return nil +function button:set(p) +end + +--- +--- Checkbox widget +---@class Checkbox:Object +--- +local checkbox = {} + +--- set method +--- @param p CheckboxStyle +--- @return nil +function checkbox:set(p) +end + +--- +--- Get the text of a label +--- @return string +function checkbox:get_text() +end + +--- +--- Dropdown widget +---@class Dropdown:Object +--- +local dropdown = {} + +--- set method +--- @param p DropdownStyle +--- @return nil +function dropdown:set(p) +end + +--- Gets an attribute of the dropdown. +--- @param which string Which property to retrieve. Valid values are "list", "options", "selected", "option_cnt", "selected_str", "option_index", "symbol", or "dir" +--- @param arg ? string +--- @return string | Dir | Object +function dropdown:get(which, arg) +end + +--- Open the drop down list +function dropdown:open() +end + +--- Close (Collapse) the drop-down list +function dropdown:close() +end + +--- Tells whether the list is opened or not +function dropdown:is_open() +end + +--- Add an options to a drop-down list from a string +--- @param option string +--- @param pos integer +function dropdown:add_option(option, pos) +end + +--- Tells whether the list is opened or not +function dropdown:clear_option() +end + +--- +--- Image widget +---@class Image:Object +--- +local img = {} + +--- Image set method +--- @param p ImageStyle +--- @return nil +function img:set(p) +end + +--- set image source +--- @param src string image source path +--- @return nil +function img:set_src(src) +end + +--- set image offset +--- img:set_offset{x = 0, y = 100} +--- @param p table +--- @return nil +function img:set_offset(p) +end + +--- set image pivot +--- img:set_pivot{x = 0, y = 100} +--- @param p table +--- @return nil +function img:set_pivot(p) +end + +--- get image size, return w,h +--- w, h = img:get_img_size() +--- w, h = img:get_img_size("/path/to/this/image.png") +--- @param src ? string +--- @return integer, integer +function img:get_img_size(src) +end + +--- +--- Label widget +---@class Label: Object +--- +local label = {} + +--- Image set method +--- @param p LabelStyle +--- @return nil +function label:set(p) +end + +--- +--- Get the text of a label +--- @return string +function label:get_text() +end + +--- +--- Get the long mode of a label +--- @return string +function label:get_long_mode() +end + +--- +--- Get the recoloring attribute +--- @return string +function label:get_recolor() +end + +--- +--- Insert a text to a label. +--- @param pos integer +--- @param txt string +--- @return nil +function label:ins_text(pos, txt) +end + +--- +--- Delete characters from a label. +--- @param pos integer +--- @param cnt integer +--- @return nil +function label:cut_text(pos, cnt) +end + +--- +--- Slider widget +---@class Slider:Object +--- +local slider = {} + +--- set method for slider widget. Uses Bar widget's properties. +--- @param p BarStyle +--- @return nil +function slider:set(p) +end + +--- get value of slider +--- @return integer +function slider:value() +end + +--- get whether slider is dragged or not +--- @return boolean +function slider:is_dragged() +end + +--- +--- Switch widget +---@class Switch:Object +--- +local switch = {} + +--- set method for switch widget +--- @param p StyleProp +--- @return nil +function switch:set(p) +end + +--- get checked state of switch +--- @return boolean +function switch:enabled() +end + +--- +--- Textarea widget +---@class Textarea: Object +--- +local textarea = {} + +--- Textarea set method +--- @param p TextareaStyle +--- @return nil +function textarea:set(p) +end + +--- get textarea text +--- @return string +function textarea:get_text(p) +end + +--- +--- Keyboard widget +---@class Keyboard: Object based on btnmatrix object +--- +local keyboard = {} + +--- Keyboard set method +--- @param p KeyboardStyle +--- @return nil +function keyboard:set(p) +end + +--- +--- LED widget +---@class Led: Object +--- +local led = {} + +--- LED set method +--- @param p LedStyle +--- @return nil +function led:set(p) +end + +--- LED set to ON +--- @return nil +function led:on() +end + +--- LED set to OFF +--- @return nil +function led:off() +end + +--- toggle LED status +--- @return nil +function led:toggle() +end + +--- get LED brightness +--- @return integer +function led:get_brightness() +end + +--- +--- List widget +---@class List: Object +--- +local list = {} + +--- List set method +--- @param p ListStyle +--- @return nil +function list:set(p) +end + +--- add text to list +--- @param text string +--- @return Label +function list:add_text(text) +end + +--- add button to list +--- @param icon ImgSrc | nil +--- @param text? string +--- @return Object a button object +function list:add_btn(icon, text) +end + +--- get list button text +--- @param btn Object +--- @return string +function list:get_btn_text(btn) +end + +--- +--- Roller widget +---@class Roller: Object +--- +local roller = {} + +--- Roller set method +--- @param p RollerStyle +--- @return nil +function roller:set(p) +end + +--- Get the options of a roller +--- @return string +function roller:get_options() +end + +--- Get the index of the selected option +--- @return integer +function roller:get_selected() +end + +--- Get the current selected option as a string. +--- @return string +function roller:get_selected_str() +end + +--- Get the total number of options +--- @return integer +function roller:get_options_cnt() +end + +--- +--- Anim +---@class Anim +--- +local Anim = {} + +--- start animation +--- @return nil +function Anim:start() +end + +--- set animation new parameters +--- @param para AnimPara new animation parameters +--- @return nil +function Anim:set(para) +end + +--- stop animation +--- @return nil +function Anim:stop() +end + +--- delete animation +--- @return nil +function Anim:delete() +end + +--- +--- Timer +---@class Timer +--- +local timer = {} + +--- set timer property +--- @param p TimerPara +--- @return nil +function timer:set(p) +end + +--- resume timer +--- @return nil +function timer:resume() +end + +--- pause timer +--- @return nil +function timer:pause() +end + +--- delete timer +--- @return nil +function timer:delete() +end + +--- make timer ready now, cb will be made soon on next loop +--- @return nil +function timer:ready() +end + +--[[ +Font is a light userdata that can be uset to set style text_font. +]] +--- @class Font +--- + +local font = {} + +--- +--- @class Style : lightuserdata +--- +local style = {} + +--- update style properties +--- @param p StyleProp +--- @return nil +function style:set(p) +end + +--- delete style, only delted style could be gc'ed +--- @return nil +function style:delete() +end + +--- remove specified property from style +--- @param p string property name from field of StyleProp +--- @return nil +function style:remove_prop(p) +end + +--- +--- Align parameter +--- @class Align +--- @field type ObjAlignType +--- @field x_ofs integer +--- @field y_ofs integer + +--- AlignTo parameter +--- @class AlignToPara +--- @field type ObjAlignType +--- @field base Object +--- @field x_ofs integer + +--- Style properties +--- @class StyleProp +--- @field w? integer +--- @field width? integer +--- @field min_width? integer +--- @field max_width? integer +--- @field height? integer +--- @field min_height? integer +--- @field max_height? integer +--- @field x? integer +--- @field y? integer +--- @field size? integer set size is equivalent to set w/h to same value +--- @field align? Align | ObjAlignType +--- @field transform_width? integer +--- @field transform_height? integer +--- @field translate_x? integer +--- @field translate_y? integer +--- @field transform_zoom? integer +--- @field transform_angle? integer +--- @field transform_pivot_x? integer +--- @field transform_pivot_y? integer +--- @field pad_all? integer +--- @field pad_top? integer +--- @field pad_bottom? integer +--- @field pad_ver? integer +--- @field pad_left? integer +--- @field pad_right? integer +--- @field pad_hor? integer +--- @field pad_row? integer +--- @field pad_column? integer +--- @field pad_gap? integer +--- @field bg_color? integer | string text color in hex integer or #RGB or #RRGGBB format +--- @field bg_opa? integer +--- @field bg_grad_color? integer +--- @field bg_grad_dir? integer +--- @field bg_main_stop? integer +--- @field bg_grad_stop? integer +--- @field bg_dither_mode? integer +--- @field bg_img_src? integer +--- @field bg_img_opa? integer +--- @field bg_img_recolor? integer +--- @field bg_img_recolor_opa? integer +--- @field bg_img_tiled? integer +--- @field border_color? integer | string +--- @field border_opa? integer +--- @field border_width? integer +--- @field border_side? integer +--- @field border_post? integer +--- @field outline_width? integer +--- @field outline_color? integer | string +--- @field outline_opa? integer +--- @field outline_pad? integer +--- @field shadow_width? integer +--- @field shadow_ofs_x? integer +--- @field shadow_ofs_y? integer +--- @field shadow_spread? integer +--- @field shadow_color? integer | string +--- @field shadow_opa? integer +--- @field img_opa? integer +--- @field img_recolor? integer +--- @field img_recolor_opa? integer +--- @field line_width? integer +--- @field line_dash_width? integer +--- @field line_dash_gap? integer +--- @field line_rounded? integer +--- @field line_color? integer | string +--- @field line_opa? integer +--- @field arc_width? integer +--- @field arc_rounded? integer +--- @field arc_color? integer | string +--- @field arc_opa? integer +--- @field arc_img_src? integer +--- @field text_color? integer | string +--- @field text_opa? integer +--- @field text_font? Font | BuiltinFont +--- @field text_letter_space? integer +--- @field text_line_space/ integer +--- @field text_decor? integer +--- @field text_align? integer +--- @field radius? integer +--- @field clip_corner? integer +--- @field opa? integer +--- @field color_filter_opa? integer +--- @field anim_time? integer +--- @field anim_speed? integer +--- @field blend_mode? integer +--- @field layout? integer +--- @field base_dir? integer +--- @field flex_flow? FlexFlow +--- @field flex_main_place? FlexAlign +--- @field flex_cross_place? FlexAlign +--- @field flex_track_place/ FlexAlign +--- @field flex_grow? integer 0..255 +--- @field flex? FlexLayoutPara + +--- + +--- Object style +--- @class ObjectStyle :StyleProp +--- @field x integer +--- @field y integer +--- @field w integer +--- @field h integer +--- @field align Align | integer +--- @field align_to AlignToPara +--- @field scrollbar_mode ScrollbarMode +--- @field scroll_dir Dir +--- @field scroll_snap_x integer +--- @field scroll_snap_y integer +--- + +--- Image style +--- @class ImageStyle :StyleProp +--- @field src string +--- @field offset_x integer offset of image +--- @field offset_y integer +--- @field angle integer +--- @field zoom integer +--- @field antialias boolean +--- @field pivot table +--- + +--- Label style +--- @class LabelStyle :StyleProp +--- @field text string + +--- Bar style +--- @class BarStyle :StyleProp +--- @field range BarRangePara +--- @field value integer + +--- Button style +--- @class ButtonStyle :StyleProp + +--- Checkbox style +--- @class CalendarStyle :StyleProp +--- @field today CalendarDatePara +--- @field showed CalendarDatePara + +--- Checkbox style +--- @class CheckboxStyle :StyleProp +--- @field text string + +--- Dropdown style +--- @class DropdownStyle :StyleProp +--- @field text string | nil +--- @field options string +--- @field selected integer +--- @field dir Dir +--- @field symbol lightuserdata | string +--- @field highlight boolean + +--- Textarea style +--- @class TextareaStyle :StyleProp +--- @field text string +--- @field placeholder string +--- @field cursor integer cursor position +--- @field password_mode boolean enable password +--- @field one_line boolean enable one line mode +--- @field password_bullet string Set the replacement characters to show in password mode +--- @field accepted_chars string DO NOT USE. Set a list of characters. Only these characters will be accepted by the text area E.g. "+-.,0123456789" +--- @field max_length integer Set max length of a Text Area. +--- @field password_show_time integer Set how long show the password before changing it to '*' + +--- Keyboard style +--- @class KeyboardStyle :StyleProp +--- @field textarea Textarea textarea object +--- @field mode KeyboardMode +--- @field popovers boolean Show the button title in a popover when pressed. + +--- Led style +--- @class LedStyle :StyleProp +--- @field color integer|string color of led +--- @field brightness integer brightness in range of 0..255 + +--- List style +--- @class ListStyle :StyleProp + +--- Roller style +--- @class RollerStyle :StyleProp +--- @field options table | string +--- @field selected table | integer +--- @field visible_cnt integer + +--- +--- Anim(for object) parameter +--- @alias AnimExecCb fun(obj:any, value:integer): nil +--- @alias AnimDoneCb fun(anim:Anim, var:any): nil + +--- @class AnimPara +--- @field run boolean run this anim right now, or later using anim:start(). default: false +--- @field start_value integer start value +--- @field end_value integer +--- @field duration integer Anim duration in milisecond +--- @field delay integer Set a delay before starting the animation +--- @field repeat_count integer Anim repeat count, default: 1, set to 0 to disable repeat, set to lvgl.ANIM_REPEAT_INFINITE for infinite repeat, set to any other integer for specified repeate count +--- @field playback_delay integer +--- @field playback_time integer +--- @field early_apply boolean set start_value right now or not. default: true +--- @field path string | "linear" | "ease_in" | "ease_out" | "ease_in_out" | "overshoot" | "bounce" | "step" +--- @field exec_cb AnimExecCb +--- @field done_cb AnimDoneCb + + +--- +--- Timer para +--- @alias TimerCallback fun(t:Timer): nil +--- @class TimerPara +--- @field paused boolean Do not start timer immediaely +--- @field period integer timer period in ms unit +--- @field repeat_count integer | -1 | +--- @field cb TimerCallback +--- + + +--- +--- @alias ImgSrc string | lightuserdata + +--- Alignment values for flex layouts. See the [LVGL docs](https://docs.lvgl.io/8.3/layouts/flex.html#flex-align) +--- for more details. +--- @alias flexAlignOptions +---| "flex-start" +---| "flex-end" +---| "center" wow +---| "space-between" +---| "space-around" +---| "space-evenly" + +--- @class FlexLayoutPara +--- @field flex_direction? "row" | "column" | "row-reverse" | "column-reverse" +--- @field flex_wrap? "nowrap" | "wrap" | "wrap-reverse" +--- @field justify_content? flexAlignOptions +--- @field align_items? flexAlignOptions +--- @field align_content? flexAlignOptions + + +--- +--- BarRange para +--- @class BarRangePara +--- @field min integer +--- @field max integer +--- + +--- +--- CalendarToday para +--- @class CalendarDatePara +--- @field year integer +--- @field month integer +--- @field day integer +--- + +--- +--- Coordinates +--- @class Coords +--- @field x1 integer +--- @field y1 integer +--- @field x2 integer +--- @field y2 integer +--- + +return lvgl diff --git a/luals-stubs/playback.lua b/luals-stubs/playback.lua index cd54ddb3..85392e93 100644 --- a/luals-stubs/playback.lua +++ b/luals-stubs/playback.lua @@ -1,6 +1,7 @@ --- @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 --- @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. diff --git a/luals-stubs/power.lua b/luals-stubs/power.lua index 226f8200..ac7f15bb 100644 --- a/luals-stubs/power.lua +++ b/luals-stubs/power.lua @@ -1,6 +1,7 @@ --- @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 --- @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. diff --git a/luals-stubs/queue.lua b/luals-stubs/queue.lua index 08247799..353b4823 100644 --- a/luals-stubs/queue.lua +++ b/luals-stubs/queue.lua @@ -1,15 +1,31 @@ --- @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 --- @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 replay Property Whether or not the queue will be restarted after the final track is played. Writeable. --- @field repeat_track Property Whether or not the current track will repeat indefinitely. 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. +--- @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 = {} +--- 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 + +--- Moves backward in the play queue, looping back around to the end if repeat is on. function queue.previous() end return queue diff --git a/luals-stubs/screen.lua b/luals-stubs/screen.lua new file mode 100644 index 00000000..55253f1c --- /dev/null +++ b/luals-stubs/screen.lua @@ -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 diff --git a/luals-stubs/time.lua b/luals-stubs/time.lua new file mode 100644 index 00000000..95bbabdb --- /dev/null +++ b/luals-stubs/time.lua @@ -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 diff --git a/luals-stubs/types.lua b/luals-stubs/types.lua index ecfee29b..c974010d 100644 --- a/luals-stubs/types.lua +++ b/luals-stubs/types.lua @@ -1,13 +1,23 @@ --- @meta +--- A observable value, owned by the C++ firmware. ---@class Property local property = {} +--- @return integer|string|table|boolean val Returns 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? 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 ---- @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 return property diff --git a/luals-stubs/volume.lua b/luals-stubs/volume.lua index 42d65884..50046f66 100644 --- a/luals-stubs/volume.lua +++ b/luals-stubs/volume.lua @@ -1,8 +1,11 @@ --- @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 ---- @field current_pct Property ---- @field current_db Property +--- @field current_pct Property The current volume as a percentage of the current volume limit. +--- @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 = {} return volume diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index d4272c3d..05c7c216 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -13,6 +13,8 @@ #include "audio_sink.hpp" #include "bluetooth_types.hpp" +#include "cppbor.h" +#include "cppbor_parse.h" #include "esp_heap_caps.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" @@ -58,6 +60,8 @@ StreamBufferHandle_t AudioState::sDrainBuffer; std::optional AudioState::sCurrentTrack; bool AudioState::sIsPlaybackAllowed; +static std::optional> sLastTrackUpdate; + void AudioState::react(const system_fsm::BluetoothEvent& ev) { if (ev.event != drivers::bluetooth::Event::kConnectionStateChanged) { return; @@ -310,11 +314,15 @@ void Standby::react(const QueueUpdate& ev) { if (!current_track || (sCurrentTrack && (*sCurrentTrack == *current_track))) { return; } + if (ev.reason == QueueUpdate::Reason::kDeserialised && sLastTrackUpdate) { + return; + } clearDrainBuffer(); playTrack(*current_track); } static const char kQueueKey[] = "audio:queue"; +static const char kCurrentFileKey[] = "audio:current"; void Standby::react(const system_fsm::KeyLockChanged& ev) { if (!ev.locking) { @@ -332,6 +340,14 @@ void Standby::react(const system_fsm::KeyLockChanged& ev) { return; } 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) { 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(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, // but guards against bad edge cases where restoring the queue ends up // causing a crash. 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) { ESP_LOGI(kTag, "elapsed: %lu, total: %lu", ev.seconds_elapsed, ev.track->duration); + sLastTrackUpdate = std::make_pair(ev.track->filepath, ev.seconds_elapsed); } 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) { ESP_LOGI(kTag, "finished playing file"); + sLastTrackUpdate.reset(); sServices->track_queue().finish(); if (!sServices->track_queue().current()) { for (int i = 0; i < 20; i++) { diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index d55e4e0d..a8533646 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -44,6 +44,7 @@ struct QueueUpdate : tinyfsm::Event { kExplicitUpdate, kRepeatingLastTrack, kTrackFinished, + kDeserialised, }; Reason reason; }; diff --git a/src/audio/track_queue.cpp b/src/audio/track_queue.cpp index ccadd3a6..a3f4c815 100644 --- a/src/audio/track_queue.cpp +++ b/src/audio/track_queue.cpp @@ -486,7 +486,7 @@ auto TrackQueue::deserialise(const std::string& s) -> void { QueueParseClient client{*this}; const uint8_t* data = reinterpret_cast(s.data()); cppbor::parse(data, data + s.size(), &client); - notifyChanged(true, Reason::kExplicitUpdate); + notifyChanged(true, Reason::kDeserialised); } } // namespace audio diff --git a/src/battery/battery.cpp b/src/battery/battery.cpp index 95f2d17b..debef9e6 100644 --- a/src/battery/battery.cpp +++ b/src/battery/battery.cpp @@ -73,7 +73,10 @@ auto Battery::Update() -> void { } else { is_charging = *charge_state == ChargeStatus::kChargingRegular || *charge_state == ChargeStatus::kChargingFast || - *charge_state == ChargeStatus::kFullCharge; + *charge_state == ChargeStatus::kFullCharge || + // Treat 'no battery' as charging because, for UI purposes, + // we're *kind of* at full charge if u think about it. + *charge_state == ChargeStatus::kNoBattery; } if (state_ && state_->is_charging == is_charging && diff --git a/src/database/database.cpp b/src/database/database.cpp index ec11455b..ca92cf6b 100644 --- a/src/database/database.cpp +++ b/src/database/database.cpp @@ -229,13 +229,17 @@ auto Database::sizeOnDiskBytes() -> size_t { } auto Database::put(const std::string& key, const std::string& val) -> void { - db_->Put(leveldb::WriteOptions{}, kKeyCustom + key, val); + if (val.empty()) { + db_->Delete(leveldb::WriteOptions{}, kKeyCustom + key); + } else { + db_->Put(leveldb::WriteOptions{}, kKeyCustom + key, val); + } } auto Database::get(const std::string& key) -> std::optional { std::string val; auto res = db_->Get(leveldb::ReadOptions{}, kKeyCustom + key, &val); - if (!res.ok()) { + if (!res.ok() || val.empty()) { return {}; } return val; diff --git a/src/drivers/display.cpp b/src/drivers/display.cpp index cb3ee3a0..c16fc148 100644 --- a/src/drivers/display.cpp +++ b/src/drivers/display.cpp @@ -39,9 +39,6 @@ [[maybe_unused]] static const char* kTag = "DISPLAY"; -// TODO(jacqueline): Encode width and height variations in the init data. -static const uint8_t kDisplayHeight = 128 + 2; -static const uint8_t kDisplayWidth = 160 + 1; static const uint8_t kTransactionQueueSize = 2; static const gpio_num_t kDisplayDr = GPIO_NUM_33; @@ -51,9 +48,11 @@ static const gpio_num_t kDisplayCs = GPIO_NUM_22; /* * The size of each of our two display buffers. This is fundamentally a balance * between performance and memory usage. LVGL docs recommend a buffer 1/10th the - * size of the screen is the best tradeoff + * size of the screen is the best tradeoff. + 8 + * The 160x128 is the nominal size of our standard faceplate's display. */ -static const int kDisplayBufferSize = kDisplayWidth * kDisplayHeight / 10; +static const int kDisplayBufferSize = 160 * 128 / 10; DMA_ATTR static lv_color_t kDisplayBuffer[kDisplayBufferSize]; namespace drivers { @@ -154,10 +153,8 @@ auto Display::Create(IGpios& expander, lv_disp_drv_init(&display->driver_); display->driver_.draw_buf = &display->buffers_; - display->driver_.hor_res = kDisplayWidth; - display->driver_.ver_res = kDisplayHeight; - // display->driver_.sw_rotate = 1; - // display->driver_.rotated = LV_DISP_ROT_270; + display->driver_.hor_res = init_data.width; + display->driver_.ver_res = init_data.height; display->driver_.sw_rotate = 0; display->driver_.rotated = LV_DISP_ROT_NONE; display->driver_.antialiasing = 0; diff --git a/src/drivers/display_init.cpp b/src/drivers/display_init.cpp index 833ea6a4..a69826fa 100644 --- a/src/drivers/display_init.cpp +++ b/src/drivers/display_init.cpp @@ -101,6 +101,8 @@ static const uint8_t kST7735RCommonFooter[]{ // clang-format on const InitialisationData kST7735R = { + .width = 160, + .height = 128, .num_sequences = 3, .sequences = {kST7735RCommonHeader, kST7735RCommonGreen, kST7735RCommonFooter}}; diff --git a/src/drivers/gpios.cpp b/src/drivers/gpios.cpp index 5c255204..aab932a7 100644 --- a/src/drivers/gpios.cpp +++ b/src/drivers/gpios.cpp @@ -63,8 +63,8 @@ constexpr std::pair unpack(uint16_t ba) { static constexpr gpio_num_t kIntPin = GPIO_NUM_34; -auto Gpios::Create() -> Gpios* { - Gpios* instance = new Gpios(); +auto Gpios::Create(bool invert_lock) -> Gpios* { + Gpios* instance = new Gpios(invert_lock); // Read and write initial values on initialisation so that we do not have a // strange partially-initialised state. if (!instance->Flush() || !instance->Read()) { @@ -73,7 +73,10 @@ auto Gpios::Create() -> Gpios* { return instance; } -Gpios::Gpios() : ports_(pack(kPortADefault, kPortBDefault)), inputs_(0) { +Gpios::Gpios(bool invert_lock) + : ports_(pack(kPortADefault, kPortBDefault)), + inputs_(0), + invert_lock_switch_(invert_lock) { gpio_set_direction(kIntPin, GPIO_MODE_INPUT); } @@ -108,6 +111,15 @@ auto Gpios::Get(Pin pin) const -> bool { return (inputs_ & (1 << static_cast(pin))) > 0; } +auto Gpios::IsLocked() const -> bool { + bool pin = Get(Pin::kKeyLock); + if (invert_lock_switch_) { + return pin; + } else { + return !pin; + } +} + auto Gpios::Read() -> bool { uint8_t input_a, input_b; diff --git a/src/drivers/include/display_init.hpp b/src/drivers/include/display_init.hpp index f6c28b54..9bf5b3f5 100644 --- a/src/drivers/include/display_init.hpp +++ b/src/drivers/include/display_init.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include namespace drivers { @@ -14,6 +15,8 @@ namespace displays { extern const uint8_t kDelayBit; struct InitialisationData { + uint16_t width; + uint16_t height; uint8_t num_sequences; const uint8_t* sequences[4]; }; diff --git a/src/drivers/include/gpios.hpp b/src/drivers/include/gpios.hpp index 55486be7..e27a3ade 100644 --- a/src/drivers/include/gpios.hpp +++ b/src/drivers/include/gpios.hpp @@ -79,12 +79,12 @@ class IGpios { */ virtual auto Get(Pin) const -> bool = 0; - virtual auto IsLocked() const -> bool { return Get(Pin::kKeyLock); } + virtual auto IsLocked() const -> bool = 0; }; class Gpios : public IGpios { public: - static auto Create() -> Gpios*; + static auto Create(bool invert_lock_switch) -> Gpios*; ~Gpios(); /* @@ -106,6 +106,8 @@ class Gpios : public IGpios { auto Get(Pin) const -> bool override; + auto IsLocked() const -> bool override; + /** * Reads from the GPIO expander, populating `inputs` with the most recent * values. @@ -118,10 +120,11 @@ class Gpios : public IGpios { Gpios& operator=(const Gpios&) = delete; private: - Gpios(); + Gpios(bool invert_lock); std::atomic ports_; std::atomic inputs_; + const bool invert_lock_switch_; }; } // namespace drivers diff --git a/src/drivers/include/nvs.hpp b/src/drivers/include/nvs.hpp index 5bd825e5..f288f8e2 100644 --- a/src/drivers/include/nvs.hpp +++ b/src/drivers/include/nvs.hpp @@ -71,6 +71,11 @@ class NvsStorage { auto LockPolarity() -> bool; auto LockPolarity(bool) -> void; + auto DisplaySize() + -> std::pair, std::optional>; + auto DisplaySize(std::pair, std::optional>) + -> void; + auto PreferredBluetoothDevice() -> std::optional; auto PreferredBluetoothDevice(std::optional) -> void; @@ -120,6 +125,9 @@ class NvsStorage { nvs_handle_t handle_; Setting lock_polarity_; + Setting display_cols_; + Setting display_rows_; + Setting brightness_; Setting sensitivity_; Setting amp_max_vol_; diff --git a/src/drivers/nvs.cpp b/src/drivers/nvs.cpp index 875059be..28cb542c 100644 --- a/src/drivers/nvs.cpp +++ b/src/drivers/nvs.cpp @@ -37,6 +37,8 @@ static constexpr char kKeyAmpLeftBias[] = "hp_bias"; static constexpr char kKeyPrimaryInput[] = "in_pri"; static constexpr char kKeyScrollSensitivity[] = "scroll"; static constexpr char kKeyLockPolarity[] = "lockpol"; +static constexpr char kKeyDisplayCols[] = "dispcols"; +static constexpr char kKeyDisplayRows[] = "disprows"; static auto nvs_get_string(nvs_handle_t nvs, const char* key) -> std::optional { @@ -161,6 +163,8 @@ auto NvsStorage::OpenSync() -> NvsStorage* { NvsStorage::NvsStorage(nvs_handle_t handle) : handle_(handle), lock_polarity_(kKeyLockPolarity), + display_cols_(kKeyDisplayCols), + display_rows_(kKeyDisplayRows), brightness_(kKeyBrightness), sensitivity_(kKeyScrollSensitivity), amp_max_vol_(kKeyAmpMaxVolume), @@ -180,6 +184,8 @@ NvsStorage::~NvsStorage() { auto NvsStorage::Read() -> void { std::lock_guard lock{mutex_}; lock_polarity_.read(handle_); + display_cols_.read(handle_); + display_rows_.read(handle_); brightness_.read(handle_); sensitivity_.read(handle_); amp_max_vol_.read(handle_); @@ -194,6 +200,8 @@ auto NvsStorage::Read() -> void { auto NvsStorage::Write() -> bool { std::lock_guard lock{mutex_}; lock_polarity_.write(handle_); + display_cols_.write(handle_); + display_rows_.write(handle_); brightness_.write(handle_); sensitivity_.write(handle_); amp_max_vol_.write(handle_); @@ -231,6 +239,19 @@ auto NvsStorage::LockPolarity(bool p) -> void { lock_polarity_.set(p); } +auto NvsStorage::DisplaySize() + -> std::pair, std::optional> { + std::lock_guard lock{mutex_}; + return std::make_pair(display_cols_.get(), display_rows_.get()); +} + +auto NvsStorage::DisplaySize( + std::pair, std::optional> size) -> void { + std::lock_guard lock{mutex_}; + display_cols_.set(std::move(size.first)); + display_rows_.set(std::move(size.second)); +} + auto NvsStorage::PreferredBluetoothDevice() -> std::optional { std::lock_guard lock{mutex_}; diff --git a/src/drivers/samd.cpp b/src/drivers/samd.cpp index f12a18de..b631b4fb 100644 --- a/src/drivers/samd.cpp +++ b/src/drivers/samd.cpp @@ -77,29 +77,16 @@ auto Samd::UpdateChargeStatus() -> void { return; } + // FIXME: Ideally we should be using the three 'charge status' bits to work + // out whether we're actually charging, or if we've got a full charge, + // critically low charge, etc. uint8_t usb_state = raw_res & 0b11; - uint8_t charge_state = (raw_res >> 2) & 0b111; - switch (charge_state) { - case 0b000: - case 0b011: - charge_status_ = ChargeStatus::kNoBattery; - break; - case 0b001: - charge_status_ = usb_state == 1 ? ChargeStatus::kChargingRegular - : ChargeStatus::kChargingFast; - break; - case 0b010: - charge_status_ = ChargeStatus::kFullCharge; - break; - case 0b100: - charge_status_ = ChargeStatus::kBatteryCritical; - break; - case 0b101: - charge_status_ = ChargeStatus::kDischarging; - break; - default: - charge_status_ = {}; - break; + if (usb_state == 0) { + charge_status_ = ChargeStatus::kDischarging; + } else if (usb_state == 1) { + charge_status_ = ChargeStatus::kChargingRegular; + } else { + charge_status_ = ChargeStatus::kChargingFast; } } diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index 72e48aa0..0240a50c 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -5,6 +5,7 @@ idf_component_register( SRCS "lua_theme.cpp" "lua_thread.cpp" "bridge.cpp" "property.cpp" "lua_database.cpp" "lua_queue.cpp" "lua_version.cpp" "lua_theme.cpp" "lua_controls.cpp" "registry.cpp" + "lua_screen.cpp" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl" "lua-linenoise" "lua-term" diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index 44be06f8..cfa9d5f7 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -19,6 +19,7 @@ #include "lua_controls.hpp" #include "lua_database.hpp" #include "lua_queue.hpp" +#include "lua_screen.hpp" #include "lua_version.hpp" #include "lua_theme.hpp" #include "lvgl.h" @@ -86,6 +87,7 @@ auto Bridge::installBaseModules(lua_State* L) -> void { RegisterQueueModule(L); RegisterVersionModule(L); RegisterThemeModule(L); + RegisterScreenModule(L); } auto Bridge::installLvgl(lua_State* L) -> void { diff --git a/src/lua/include/lua_screen.hpp b/src/lua/include/lua_screen.hpp new file mode 100644 index 00000000..1c3bed1a --- /dev/null +++ b/src/lua/include/lua_screen.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto RegisterScreenModule(lua_State*) -> void; + +} // namespace lua diff --git a/src/lua/lua_screen.cpp b/src/lua/lua_screen.cpp new file mode 100644 index 00000000..27843bc7 --- /dev/null +++ b/src/lua/lua_screen.cpp @@ -0,0 +1,75 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_screen.hpp" + +#include +#include + +#include "lua.hpp" + +#include "esp_log.h" +#include "lauxlib.h" +#include "lua.h" +#include "lvgl.h" + +#include "bridge.hpp" +#include "database.hpp" +#include "event_queue.hpp" +#include "index.hpp" +#include "property.hpp" +#include "service_locator.hpp" +#include "track.hpp" +#include "track_queue.hpp" +#include "ui_events.hpp" + +namespace lua { + +static auto screen_new(lua_State* L) -> int { + // o = o or {} + if (lua_gettop(L) != 2) { + lua_settop(L, 1); + lua_newtable(L); + } + // Swap o and self on the stack. + lua_insert(L, 1); + + lua_pushliteral(L, "__index"); + lua_pushvalue(L, 1); + lua_settable(L, 1); // self.__index = self + + lua_setmetatable(L, 1); // setmetatable(o, self) + + return 1; // return o +} + +static auto screen_noop(lua_State* state) -> int { + return 0; +} + +static const struct luaL_Reg kScreenFuncs[] = {{"new", screen_new}, + {"createUi", screen_noop}, + {"onShown", screen_noop}, + {"onHidden", screen_noop}, + {NULL, NULL}}; + +static auto lua_screen(lua_State* state) -> int { + luaL_newlib(state, kScreenFuncs); + + lua_pushliteral(state, "__index"); + lua_pushvalue(state, -2); + lua_rawset(state, -3); + + return 1; +} + +auto RegisterScreenModule(lua_State* s) -> void { + luaL_requiref(s, "screen", lua_screen, true); + + lua_pop(s, 1); +} + +} // namespace lua diff --git a/src/system_fsm/booting.cpp b/src/system_fsm/booting.cpp index 41f46df2..eb931192 100644 --- a/src/system_fsm/booting.cpp +++ b/src/system_fsm/booting.cpp @@ -57,13 +57,22 @@ auto Booting::entry() -> void { sServices.reset(new ServiceLocator()); ESP_LOGI(kTag, "installing early drivers"); + // NVS is needed first because it contains information about what specific + // hardware configuration we're running on. + sServices->nvs( + std::unique_ptr(drivers::NvsStorage::OpenSync())); + + // HACK: fix up the switch polarity on newer dev units + sServices->nvs().LockPolarity(false); + // I2C and SPI are both always needed. We can't even power down or show an // error without these. ESP_ERROR_CHECK(drivers::init_spi()); - sServices->gpios(std::unique_ptr(drivers::Gpios::Create())); + sServices->gpios(std::unique_ptr( + drivers::Gpios::Create(sServices->nvs().LockPolarity()))); ESP_LOGI(kTag, "starting ui"); - if (!ui::UiState::InitBootSplash(sServices->gpios())) { + if (!ui::UiState::InitBootSplash(sServices->gpios(), sServices->nvs())) { events::System().Dispatch(FatalError{}); return; } @@ -74,8 +83,6 @@ auto Booting::entry() -> void { ESP_LOGI(kTag, "installing remaining drivers"); drivers::spiffs_mount(); sServices->samd(std::unique_ptr(drivers::Samd::Create())); - sServices->nvs( - std::unique_ptr(drivers::NvsStorage::OpenSync())); sServices->touchwheel( std::unique_ptr{drivers::TouchWheel::Create()}); sServices->haptics(std::make_unique()); @@ -100,8 +107,6 @@ auto Booting::entry() -> void { sServices->bluetooth().Enable(); } - sServices->nvs().LockPolarity(true); - BootComplete ev{.services = sServices}; events::Audio().Dispatch(ev); events::Ui().Dispatch(ev); diff --git a/src/system_fsm/include/system_events.hpp b/src/system_fsm/include/system_events.hpp index 32394958..1be03f82 100644 --- a/src/system_fsm/include/system_events.hpp +++ b/src/system_fsm/include/system_events.hpp @@ -57,7 +57,6 @@ struct SamdUsbMscChanged : tinyfsm::Event { bool en; }; -struct ChargingStatusChanged : tinyfsm::Event {}; struct BatteryStateChanged : tinyfsm::Event { battery::Battery::BatteryState new_state; }; diff --git a/src/system_fsm/system_fsm.cpp b/src/system_fsm/system_fsm.cpp index 977f4a6d..5a1ccf8c 100644 --- a/src/system_fsm/system_fsm.cpp +++ b/src/system_fsm/system_fsm.cpp @@ -84,10 +84,8 @@ void SystemState::react(const internal::SamdInterrupt&) { auto charge_status = samd.GetChargeStatus(); auto usb_status = samd.GetUsbStatus(); - if (charge_status != prev_charge_status) { - ChargingStatusChanged ev{}; - events::System().Dispatch(ev); - events::Ui().Dispatch(ev); + if (charge_status != prev_charge_status && sServices) { + sServices->battery().Update(); } if (usb_status != prev_usb_status) { ESP_LOGI(kTag, "usb status changed"); diff --git a/src/ui/include/screen.hpp b/src/ui/include/screen.hpp index 60939660..4241c712 100644 --- a/src/ui/include/screen.hpp +++ b/src/ui/include/screen.hpp @@ -27,6 +27,9 @@ class Screen { Screen(); virtual ~Screen(); + virtual auto onShown() -> void {} + virtual auto onHidden() -> void {} + auto root() -> lv_obj_t* { return root_; } auto content() -> lv_obj_t* { return content_; } auto alert() -> lv_obj_t* { return alert_; } diff --git a/src/ui/include/screen_lua.hpp b/src/ui/include/screen_lua.hpp index ee9f6813..0ed3a508 100644 --- a/src/ui/include/screen_lua.hpp +++ b/src/ui/include/screen_lua.hpp @@ -18,6 +18,9 @@ class Lua : public Screen { Lua(); ~Lua(); + auto onShown() -> void override; + auto onHidden() -> void override; + auto SetObjRef(lua_State*) -> void; private: diff --git a/src/ui/include/ui_fsm.hpp b/src/ui/include/ui_fsm.hpp index 6cf2ba4c..579cc2bb 100644 --- a/src/ui/include/ui_fsm.hpp +++ b/src/ui/include/ui_fsm.hpp @@ -36,7 +36,7 @@ namespace ui { class UiState : public tinyfsm::Fsm { public: - static auto InitBootSplash(drivers::IGpios&) -> bool; + static auto InitBootSplash(drivers::IGpios&, drivers::NvsStorage&) -> bool; virtual ~UiState() {} @@ -129,6 +129,7 @@ class UiState : public tinyfsm::Fsm { static lua::Property sControlsScheme; static lua::Property sScrollSensitivity; + static lua::Property sLockSwitch; static lua::Property sDatabaseUpdating; }; diff --git a/src/ui/screen.cpp b/src/ui/screen.cpp index bacce3f9..a39aaf7e 100644 --- a/src/ui/screen.cpp +++ b/src/ui/screen.cpp @@ -35,6 +35,9 @@ Screen::Screen() lv_obj_set_style_bg_opa(modal_content_, LV_OPA_TRANSP, 0); lv_obj_set_style_bg_opa(alert_, LV_OPA_TRANSP, 0); + lv_obj_set_scrollbar_mode(root_, LV_SCROLLBAR_MODE_OFF); + lv_obj_set_scrollbar_mode(content_, LV_SCROLLBAR_MODE_OFF); + // Disable wrapping by default, since it's confusing and generally makes it // harder to navigate quickly. lv_group_set_wrap(group_, false); diff --git a/src/ui/screen_lua.cpp b/src/ui/screen_lua.cpp index b3554241..1ad4a8e8 100644 --- a/src/ui/screen_lua.cpp +++ b/src/ui/screen_lua.cpp @@ -7,9 +7,11 @@ #include "screen_lua.hpp" #include "core/lv_obj_tree.h" +#include "lua.h" #include "lua.hpp" #include "themes.hpp" +#include "lua_thread.hpp" #include "luavgl.h" namespace ui { @@ -25,6 +27,40 @@ Lua::~Lua() { } } +auto Lua::onShown() -> void { + if (!s_ || !obj_ref_) { + return; + } + lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); + lua_pushliteral(s_, "onShown"); + + if (lua_gettable(s_, -2) == LUA_TFUNCTION) { + lua_pushvalue(s_, -2); + lua::CallProtected(s_, 1, 0); + } else { + lua_pop(s_, 1); + } + + lua_pop(s_, 1); +} + +auto Lua::onHidden() -> void { + if (!s_ || !obj_ref_) { + return; + } + lua_rawgeti(s_, LUA_REGISTRYINDEX, *obj_ref_); + lua_pushliteral(s_, "onHidden"); + + if (lua_gettable(s_, -2) == LUA_TFUNCTION) { + lua_pushvalue(s_, -2); + lua::CallProtected(s_, 1, 0); + } else { + lua_pop(s_, 1); + } + + lua_pop(s_, 1); +} + auto Lua::SetObjRef(lua_State* s) -> void { assert(s_ == nullptr); s_ = s; diff --git a/src/ui/ui_fsm.cpp b/src/ui/ui_fsm.cpp index 25ae9817..5582dabe 100644 --- a/src/ui/ui_fsm.cpp +++ b/src/ui/ui_fsm.cpp @@ -12,6 +12,7 @@ #include "bluetooth_types.hpp" #include "db_events.hpp" +#include "display_init.hpp" #include "freertos/portmacro.h" #include "freertos/projdefs.h" #include "lua.h" @@ -125,21 +126,24 @@ lua::Property UiState::sPlaybackPlaying{ }}; lua::Property UiState::sPlaybackTrack{}; -lua::Property UiState::sPlaybackPosition{0, [](const lua::LuaValue& val) { - int current_val = std::get(sPlaybackPosition.Get()); - if (!std::holds_alternative(val)) { - return false; - } - int new_val = std::get(val); - if (current_val != new_val) { - auto track = sPlaybackTrack.Get(); - if (!std::holds_alternative(track)) { +lua::Property UiState::sPlaybackPosition{ + 0, [](const lua::LuaValue& val) { + int current_val = std::get(sPlaybackPosition.Get()); + if (!std::holds_alternative(val)) { return false; } - events::Audio().Dispatch(audio::SeekFile{.offset = (uint32_t)new_val, .filename = std::get(track).filepath}); - } - return true; -}}; + int new_val = std::get(val); + if (current_val != new_val) { + auto track = sPlaybackTrack.Get(); + if (!std::holds_alternative(track)) { + return false; + } + events::Audio().Dispatch(audio::SeekFile{ + .offset = (uint32_t)new_val, + .filename = std::get(track).filepath}); + } + return true; + }}; lua::Property UiState::sQueuePosition{0}; lua::Property UiState::sQueueSize{0}; @@ -277,12 +281,25 @@ lua::Property UiState::sScrollSensitivity{ return true; }}; +lua::Property UiState::sLockSwitch{false}; + lua::Property UiState::sDatabaseUpdating{false}; -auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { +auto UiState::InitBootSplash(drivers::IGpios& gpios, drivers::NvsStorage& nvs) + -> bool { // Init LVGL first, since the display driver registers itself with LVGL. lv_init(); - sDisplay.reset(drivers::Display::Create(gpios, drivers::displays::kST7735R)); + + drivers::displays::InitialisationData init_data = drivers::displays::kST7735R; + + // HACK: correct the display size for our prototypes. + // nvs.DisplaySize({161, 130}); + + auto actual_size = nvs.DisplaySize(); + init_data.width = actual_size.first.value_or(init_data.width); + init_data.height = actual_size.second.value_or(init_data.height); + + sDisplay.reset(drivers::Display::Create(gpios, init_data)); if (sDisplay == nullptr) { return false; } @@ -294,27 +311,36 @@ auto UiState::InitBootSplash(drivers::IGpios& gpios) -> bool { } void UiState::PushScreen(std::shared_ptr screen) { + lv_obj_set_parent(sAlertContainer, screen->alert()); + if (sCurrentScreen) { + sCurrentScreen->onHidden(); sScreens.push(sCurrentScreen); } sCurrentScreen = screen; - lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert()); + sCurrentScreen->onShown(); } int UiState::PopScreen() { if (sScreens.empty()) { return 0; } - sCurrentScreen = sScreens.top(); - lv_obj_set_parent(sAlertContainer, sCurrentScreen->alert()); + lv_obj_set_parent(sAlertContainer, sScreens.top()->alert()); + + sCurrentScreen->onHidden(); + sCurrentScreen = sScreens.top(); sScreens.pop(); + + sCurrentScreen->onShown(); + return sScreens.size(); } void UiState::react(const system_fsm::KeyLockChanged& ev) { sDisplay->SetDisplayOn(!ev.locking); sInput->lock(ev.locking); + sLockSwitch.Update(ev.locking); } void UiState::react(const internal::ControlSchemeChanged&) { @@ -506,6 +532,7 @@ void Lua::entry() { { {"scheme", &sControlsScheme}, {"scroll_sensitivity", &sScrollSensitivity}, + {"lock_switch", &sLockSwitch}, }); registry.AddPropertyModule( @@ -540,7 +567,7 @@ void Lua::entry() { auto Lua::PushLuaScreen(lua_State* s) -> int { // Ensure the arg looks right before continuing. - luaL_checktype(s, 1, LUA_TFUNCTION); + luaL_checktype(s, 1, LUA_TTABLE); // First, create a new plain old Screen object. We will use its root and // group for the Lua screen. Allocate it in external ram so that arbitrarily @@ -555,10 +582,15 @@ auto Lua::PushLuaScreen(lua_State* s) -> int { lv_group_set_default(new_screen->group()); // Call the constructor for this screen. - lua_settop(s, 1); // Make sure the function is actually at top of stack - lua::CallProtected(s, 0, 1); + // lua_settop(s, 1); // Make sure the screen is actually at top of stack + lua_pushliteral(s, "createUi"); + if (lua_gettable(s, 1) == LUA_TFUNCTION) { + lua_pushvalue(s, 1); + lua::CallProtected(s, 1, 0); + } - // Store the reference for the table the constructor returned. + // Store the reference for this screen's table. + lua_settop(s, 1); new_screen->SetObjRef(s); // Finally, push the now-initialised screen as if it were a regular C++ @@ -586,7 +618,7 @@ auto Lua::PopLuaScreen(lua_State* s) -> int { } auto Lua::Ticks(lua_State* s) -> int { - lua_pushinteger(s, esp_timer_get_time()/1000); + lua_pushinteger(s, esp_timer_get_time() / 1000); return 1; } diff --git a/tools/cmake/common.cmake b/tools/cmake/common.cmake index 69013448..80436023 100644 --- a/tools/cmake/common.cmake +++ b/tools/cmake/common.cmake @@ -5,7 +5,7 @@ # For more information about build system see # https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html -set(PROJECT_VER "0.7.0") +set(PROJECT_VER "0.7.1") # esp-idf sets the C++ standard weird. Set cmake vars to match. set(CMAKE_CXX_STANDARD 23) diff --git a/tools/luals-gendoc/gendoc.lua b/tools/luals-gendoc/gendoc.lua new file mode 100755 index 00000000..ace0b6c9 --- /dev/null +++ b/tools/luals-gendoc/gendoc.lua @@ -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 diff --git a/tools/luals-gendoc/json.lua b/tools/luals-gendoc/json.lua new file mode 100644 index 00000000..711ef786 --- /dev/null +++ b/tools/luals-gendoc/json.lua @@ -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