Load one picture at a time on a Guition: an alert's picture before a media tile's cover (0.2.77)
A download shares ESPHome's loop with touch and drawing. An alert already closes every card, and the card's cover with it; a media tile over the whole page stays under the alert. Its cover now waits while the alert's picture is announced, loading or due for a retry, and an alert picture that arrives while a cover is on its way (or a dropped cover's download is still being closed) starts right after it. The host harness checks all three cases on the Guition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3756,10 +3756,22 @@ inline lv_obj_t *camera_root = nullptr, *camera_picture = nullptr, *camera_note
|
|||||||
inline lv_obj_t *alert_frame = nullptr, *alert_picture = nullptr, *alert_frame_icon = nullptr;
|
inline lv_obj_t *alert_frame = nullptr, *alert_picture = nullptr, *alert_frame_icon = nullptr;
|
||||||
inline std::function<void(bool)> alert_room; // the board makes the card taller for the frame, or back
|
inline std::function<void(bool)> alert_room; // the board makes the card taller for the frame, or back
|
||||||
inline std::string alert_announced, alert_camera, alert_url;
|
inline std::string alert_announced, alert_camera, alert_url;
|
||||||
inline uint32_t alert_announced_at = 0, alert_retry_at = 0;
|
inline uint32_t alert_announced_at = 0, alert_retry_at = 0, alert_shown_at = 0;
|
||||||
inline uint8_t alert_retries = 0;
|
inline uint8_t alert_retries = 0;
|
||||||
inline bool alert_thumb_loading = false;
|
inline bool alert_thumb_loading = false;
|
||||||
constexpr uint8_t ALERT_IMAGE_RETRIES = 3; // an alert's picture is worth another try after a failed connection
|
constexpr uint8_t ALERT_IMAGE_RETRIES = 3; // an alert's picture is worth another try after a failed connection
|
||||||
|
// One picture loads at a time (firmware 0.2.64+): a download shares ESPHome's loop with touch and drawing, and two at
|
||||||
|
// once (a cover and an alert's picture) would double the time a tap can wait. An alert closes every card (wake_display
|
||||||
|
// runs close_cards), so the card's cover goes with it; a media tile over the whole page stays under the alert. Its
|
||||||
|
// cover waits while the alert's picture is announced, on its way or about to be tried again; a cover already on its
|
||||||
|
// way finishes and the alert's picture starts right after it. A full camera and the cover share one image, so they
|
||||||
|
// never load together. Memory is not the limit here: the pictures live in PSRAM (a cover ~70 KB, the alert's 172 KB).
|
||||||
|
inline bool alert_image_due() {
|
||||||
|
if (alert_camera.empty() || alert_picture) return false;
|
||||||
|
if (alert_thumb_loading || alert_retry_at) return true;
|
||||||
|
// The link has not come yet: wait for it as long as an announced camera still belongs to its alert.
|
||||||
|
return alert_url.empty() && esphome::millis() - alert_shown_at < camera_view::PENDING_MS;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool camera_supported() { return static_cast<bool>(camera_full.load); }
|
inline bool camera_supported() { return static_cast<bool>(camera_full.load); }
|
||||||
inline bool camera_visible() { return camera_root != nullptr; }
|
inline bool camera_visible() { return camera_root != nullptr; }
|
||||||
@@ -3835,6 +3847,7 @@ inline void cover_tick(uint32_t now) {
|
|||||||
if (!cover_visible()) { cover_drop(); return; }
|
if (!cover_visible()) { cover_drop(); return; }
|
||||||
if (!awake()) return;
|
if (!awake()) return;
|
||||||
if (!cover.open()) cover.open(cover_wish.entity, true);
|
if (!cover.open()) cover.open(cover_wish.entity, true);
|
||||||
|
if (alert_image_due()) return; // the alert's picture first
|
||||||
if (cover.should_ask(now)) {
|
if (cover.should_ask(now)) {
|
||||||
if (!fresh()) return;
|
if (!fresh()) return;
|
||||||
cover.ask(now);
|
cover.ask(now);
|
||||||
@@ -3843,6 +3856,7 @@ inline void cover_tick(uint32_t now) {
|
|||||||
auto *input = lv_indev_get_next(nullptr);
|
auto *input = lv_indev_get_next(nullptr);
|
||||||
if (input && lv_indev_get_state(input) == LV_INDEV_STATE_PRESSED) return;
|
if (input && lv_indev_get_state(input) == LV_INDEV_STATE_PRESSED) return;
|
||||||
cover.start(now);
|
cover.start(now);
|
||||||
|
ESP_LOGI("camera", "cover load %s", cover.entity.c_str());
|
||||||
camera_full.load(cover.url);
|
camera_full.load(cover.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3950,10 +3964,12 @@ inline void camera_open(const std::string &entity, const std::string &name) {
|
|||||||
inline void camera_tick() {
|
inline void camera_tick() {
|
||||||
const uint32_t now = esphome::millis();
|
const uint32_t now = esphome::millis();
|
||||||
if (camera_release_due && !camera_root) camera_release();
|
if (camera_release_due && !camera_root) camera_release();
|
||||||
if (alert_retry_at && now >= alert_retry_at) {
|
// A retry, or an alert's picture that waited for a cover on its way (one picture at a time).
|
||||||
|
if (alert_retry_at && now >= alert_retry_at && !cover.loading) {
|
||||||
alert_retry_at = 0;
|
alert_retry_at = 0;
|
||||||
if (!alert_camera.empty() && !alert_url.empty() && camera_thumb.load) {
|
if (!alert_camera.empty() && !alert_url.empty() && camera_thumb.load) {
|
||||||
alert_thumb_loading = true;
|
alert_thumb_loading = true;
|
||||||
|
ESP_LOGI("camera", "alert picture load");
|
||||||
camera_thumb.load(alert_url);
|
camera_thumb.load(alert_url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3987,6 +4003,7 @@ inline void alert_prepare() {
|
|||||||
const bool with_image = camera_supported() && alert_frame && !alert_announced.empty() &&
|
const bool with_image = camera_supported() && alert_frame && !alert_announced.empty() &&
|
||||||
esphome::millis() - alert_announced_at < camera_view::PENDING_MS;
|
esphome::millis() - alert_announced_at < camera_view::PENDING_MS;
|
||||||
alert_camera = with_image ? alert_announced : std::string();
|
alert_camera = with_image ? alert_announced : std::string();
|
||||||
|
alert_shown_at = esphome::millis();
|
||||||
alert_url.clear();
|
alert_url.clear();
|
||||||
alert_announced.clear();
|
alert_announced.clear();
|
||||||
if (alert_frame) {
|
if (alert_frame) {
|
||||||
@@ -4027,7 +4044,15 @@ inline void camera_answer(const std::string &view, const std::string &entity, co
|
|||||||
alert_picture_clear();
|
alert_picture_clear();
|
||||||
alert_url = url;
|
alert_url = url;
|
||||||
alert_retries = 0;
|
alert_retries = 0;
|
||||||
|
// A cover on its way finishes first, and a dropped one is closed first (on the next tick); camera_tick starts this
|
||||||
|
// one right after.
|
||||||
|
if (cover.loading || camera_release_due) {
|
||||||
|
alert_retry_at = esphome::millis() | 1;
|
||||||
|
ESP_LOGI("camera", "alert picture waits for the cover");
|
||||||
|
return;
|
||||||
|
}
|
||||||
alert_thumb_loading = true;
|
alert_thumb_loading = true;
|
||||||
|
ESP_LOGI("camera", "alert picture load");
|
||||||
camera_thumb.load(url);
|
camera_thumb.load(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4059,11 +4084,12 @@ inline void camera_loaded(bool thumb, bool cached) {
|
|||||||
if (alert_camera.empty() || !alert_frame) return;
|
if (alert_camera.empty() || !alert_frame) return;
|
||||||
camera_show(alert_frame, alert_picture, camera_thumb.source(), !cached);
|
camera_show(alert_frame, alert_picture, camera_thumb.source(), !cached);
|
||||||
if (alert_picture && alert_frame_icon) lv_obj_add_flag(alert_frame_icon, LV_OBJ_FLAG_HIDDEN);
|
if (alert_picture && alert_frame_icon) lv_obj_add_flag(alert_frame_icon, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
ESP_LOGI("camera", "alert picture shown");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!camera_root) {
|
if (!camera_root) {
|
||||||
// The media card's cover (firmware 0.2.64+): the same online_image, loaded once.
|
// The media card's cover (firmware 0.2.64+): the same online_image, loaded once.
|
||||||
if (cover.loading) { cover.finish(esphome::millis(), true); cover_arrived(); }
|
if (cover.loading) { cover.finish(esphome::millis(), true); ESP_LOGI("camera", "cover loaded"); cover_arrived(); }
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!camera.loading) return; // a cover's download that ended after the camera opened: not this camera's picture
|
if (!camera.loading) return; // a cover's download that ended after the camera opened: not this camera's picture
|
||||||
@@ -4080,6 +4106,7 @@ inline void camera_loaded(bool thumb, bool cached) {
|
|||||||
inline void camera_failed(bool thumb) {
|
inline void camera_failed(bool thumb) {
|
||||||
if (thumb) {
|
if (thumb) {
|
||||||
alert_thumb_loading = false;
|
alert_thumb_loading = false;
|
||||||
|
ESP_LOGI("camera", "alert picture failed");
|
||||||
if (!alert_camera.empty() && !alert_picture && alert_retries < ALERT_IMAGE_RETRIES) {
|
if (!alert_camera.empty() && !alert_picture && alert_retries < ALERT_IMAGE_RETRIES) {
|
||||||
++alert_retries;
|
++alert_retries;
|
||||||
alert_retry_at = esphome::millis() + 1500;
|
alert_retry_at = esphome::millis() + 1500;
|
||||||
@@ -4087,7 +4114,7 @@ inline void camera_failed(bool thumb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!camera_root) {
|
if (!camera_root) {
|
||||||
if (cover.loading) cover.finish(esphome::millis(), false); // tried again after the gap, three times at most
|
if (cover.loading) { cover.finish(esphome::millis(), false); ESP_LOGI("camera", "cover failed"); } // tried again after the gap, three times at most
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!camera.loading) return;
|
if (!camera.loading) return;
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ what plays, with the title, the artist and the album, a progress bar and the key
|
|||||||
- A player without a picture (a radio station, a player that is off) keeps the player's icon in
|
- A player without a picture (a radio station, a player that is off) keeps the player's icon in
|
||||||
the cover's place; the app answers with an empty link and the screen stops asking.
|
the cover's place; the app answers with an empty link and the screen stops asking.
|
||||||
- The CYD shows the same card without the picture: its icon stands in for the cover.
|
- The CYD shows the same card without the picture: its icon stands in for the cover.
|
||||||
|
- One picture loads at a time. An alert closes an open card and its cover. Under a media tile of
|
||||||
|
size *Full page* the alert's picture goes first: the tile's cover waits until the alert's picture
|
||||||
|
is there, and a cover already on its way finishes before the alert's picture starts.
|
||||||
|
|
||||||
## A doorbell
|
## A doorbell
|
||||||
|
|
||||||
|
|||||||
+23
-10
@@ -21,6 +21,13 @@ See CHANGELOG 0.2.77. Built on 0.2.76 (`bdb1f12`).
|
|||||||
screen takes the shared image buffer and the cover comes back after. The camera answer parser accepts `t: cover`.
|
screen takes the shared image buffer and the cover comes back after. The camera answer parser accepts `t: cover`.
|
||||||
The palette pass leaves the media tile's parts alone (it painted them the card's grey). A player's state falls
|
The palette pass leaves the media tile's parts alone (it painted them the card's grey). A player's state falls
|
||||||
back to the screen's own word ("Playing") when Home Assistant sent none.
|
back to the screen's own word ("Playing") when Home Assistant sent none.
|
||||||
|
- One picture at a time (asked by Max after approving the renders: "two images at once, is that too much?"). An
|
||||||
|
alert already closes every card (`wake_display` runs `close_cards`), so the card's cover is dropped with it. A
|
||||||
|
media tile over the whole page stays under the alert: `alert_image_due()` holds its cover while the alert's
|
||||||
|
picture is announced, loading or due for a retry, and an alert picture that arrives while a cover is on its way
|
||||||
|
(or while a dropped cover's download is still being closed) waits for it (`alert_retry_at`, started by
|
||||||
|
`camera_tick`). Memory was never the limit: the pictures live in PSRAM (cover ~70 KB, alert 172 KB); the rule
|
||||||
|
keeps the main loop, where touch waits, to one download.
|
||||||
- `runtime_model.h`: `Extra` carries artist, album, picture mark, duration, position and its moment.
|
- `runtime_model.h`: `Extra` carries artist, album, picture mark, duration, position and its moment.
|
||||||
- `camera_view.h`: `open(entity, once)`, `loaded`; an empty answer to a one-time feed stops the asking.
|
- `camera_view.h`: `open(entity, once)`, `loaded`; an empty answer to a one-time feed stops the asking.
|
||||||
- `theme.h`: `theme::of(lv_color_t)` (the 24-bit value of a drawn colour, for the cover's corner colour).
|
- `theme.h`: `theme::of(lv_color_t)` (the 24-bit value of a drawn colour, for the cover's corner colour).
|
||||||
@@ -39,7 +46,7 @@ See CHANGELOG 0.2.77. Built on 0.2.76 (`bdb1f12`).
|
|||||||
fetch-on-change and links, the app's answer through `Manager.answer_camera` (Guition 0.2.64 gets a link, a stream
|
fetch-on-change and links, the app's answer through `Manager.answer_camera` (Guition 0.2.64 gets a link, a stream
|
||||||
an empty one, a stranger nothing, older firmware and a CYD nothing). Whole Python suite: 409 + 14, pass.
|
an empty one, a stranger nothing, older firmware and a CYD nothing). Whole Python suite: 409 + 14, pass.
|
||||||
- Host harness (`.esphome/readme-render/media/media_host.py`, real ESPHome API, virtual finger, the app's own
|
- Host harness (`.esphome/readme-render/media/media_host.py`, real ESPHome API, virtual finger, the app's own
|
||||||
`answer_camera` behind `/dev/cover`): **Guition 57/57, CYD 42/42.** Covered: the card of a playing player (title,
|
`answer_camera` behind `/dev/cover`): **Guition 64/64, CYD 42/42.** Covered: the card of a playing player (title,
|
||||||
artist · album, elapsed from the reported position and running on, keys enabled, slider at 32 %, bar track and
|
artist · album, elapsed from the reported position and running on, keys enabled, slider at 32 %, bar track and
|
||||||
fill), the cover asked for with size and page colour and loaded once over the placeholder, pause → `media_play_pause`
|
fill), the cover asked for with size and page colour and loaded once over the placeholder, pause → `media_play_pause`
|
||||||
with the keys faded during the wait and back after, next, mute (`is_volume_muted: true`), the slider drag →
|
with the keys faded during the wait and back after, next, mute (`is_volume_muted: true`), the slider drag →
|
||||||
@@ -52,6 +59,10 @@ See CHANGELOG 0.2.77. Built on 0.2.76 (`bdb1f12`).
|
|||||||
card which takes the cover at its own size, closing gives it back to the tile); a stream over the page (no times,
|
card which takes the cover at its own size, closing gives it back to the tile); a stream over the page (no times,
|
||||||
no cover, the study cover dropped); an idle player ("Not playing"); a wide media tile with a volume control keeps
|
no cover, the study cover dropped); an idle player ("Not playing"); a wide media tile with a volume control keeps
|
||||||
its panel; no errors in the firmware log. The CYD asks for no cover anywhere.
|
its panel; no errors in the firmware log. The CYD asks for no cover anywhere.
|
||||||
|
Alerts with a camera (Guition): an alert closes the open media card and drops its cover, and its picture loads
|
||||||
|
alone; under a full-page media tile whose cover is not loaded yet, the cover waits until the alert's picture is
|
||||||
|
shown and loads after it; a cover on its way (a link that answers after 1.5 s) finishes first and the alert's
|
||||||
|
picture ("waits for the cover") follows. The firmware log shows the downloads one after the other in all three.
|
||||||
- Renders (`render_media.py`, both boards, light and dark): card of a playing player, a stream, a paused TV, an off
|
- Renders (`render_media.py`, both boards, light and dark): card of a playing player, a stream, a paused TV, an off
|
||||||
player, tiles over the page (study, radio, idle). Sent to Max for approval.
|
player, tiles over the page (study, radio, idle). Sent to Max for approval.
|
||||||
- ESP32 builds of both Easy Setup profiles: see the sizes below.
|
- ESP32 builds of both Easy Setup profiles: see the sizes below.
|
||||||
@@ -59,12 +70,12 @@ See CHANGELOG 0.2.77. Built on 0.2.76 (`bdb1f12`).
|
|||||||
|
|
||||||
## Sizes
|
## Sizes
|
||||||
|
|
||||||
Both Easy Setup profiles compile with ESPHome 2026.6.2 (on Python 3.11, see the pitfalls), no warnings.
|
Both Easy Setup profiles compile with ESPHome 2026.6.2, no warnings (see the pitfalls for this Mac's Python).
|
||||||
|
|
||||||
| Build | Flash | RAM |
|
| Build | Flash | RAM |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Guition (easy-guition-device) | 1,971,583 B, 24.3 % of 8 MB | 76,844 B, 23.5 % |
|
| Guition (easy-guition-device) | 1,971,975 B, 24.3 % of 8 MB | 76,852 B, 23.5 % |
|
||||||
| CYD (easy-cyd-device) | 1,582,063 B, 86.2 % of the 1.835 MB update slot | 71,276 B, 21.8 % |
|
| CYD (easy-cyd-device) | 1,582,183 B, 86.2 % of the 1.835 MB update slot | 71,276 B, 21.8 % |
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|
||||||
@@ -77,9 +88,11 @@ Pending at the time of writing.
|
|||||||
- LVGL's `LV_LABEL_LONG_DOT` writes the dots into the label's text: a probe sees "Miles Davis · Kin...".
|
- LVGL's `LV_LABEL_LONG_DOT` writes the dots into the label's text: a probe sees "Miles Davis · Kin...".
|
||||||
- `tick()` re-enables every object in `detail_actions` after a wait: a key the player lacks must stay out of it.
|
- `tick()` re-enables every object in `detail_actions` after a wait: a key the player lacks must stay out of it.
|
||||||
- The tile palette pass paints every custom part; a mode that paints its own parts must be skipped there.
|
- The tile palette pass paints every custom part; a mode that paints its own parts must be skipped there.
|
||||||
- Homebrew's python@3.14.7 on this Mac has a broken `pyexpat` (it wants a libexpat symbol macOS 26.2 lacks), so
|
- Homebrew's python@3.14.7 on this Mac has a broken `pyexpat` (it loads macOS's `/usr/lib/libexpat.1.dylib`, which
|
||||||
`platform.mac_ver()` returns '' and both pip's truststore and `uv` refuse the interpreter. The pioarduino platform
|
lacks `_XML_SetAllocTrackerActivationThreshold`), so `platform.mac_ver()` returns '' and pip's truststore and `uv`
|
||||||
recreates `~/.platformio/penv` from the running Python and then cannot install into it. The ESP32 builds of this
|
refuse the interpreter. The pioarduino platform then cannot rebuild `~/.platformio/penv` ("Failed to install Python
|
||||||
release ran from an ESPHome 2026.6.2 environment on PlatformIO's own bundled Python 3.11 (`.esphome/venv311`,
|
dependencies into penv"). Setting `DYLD_LIBRARY_PATH=/opt/homebrew/opt/expat/lib` is not enough, because
|
||||||
local); `brew reinstall python@3.14` (or expat) is the real fix. Also: never run two ESPHome builds at once, the
|
pioarduino starts `uv` without it. What works: ESPHome 2026.6.2 in a venv on PlatformIO's own bundled Python 3.11
|
||||||
second one clears the penv the first one uses.
|
(`~/.platformio/python3`), where `mac_ver()` is fine. Every session must build with that same Python: pioarduino
|
||||||
|
recreates the penv whenever the Python version changes, so a build on another Python breaks the next one elsewhere.
|
||||||
|
`brew reinstall python@3.14` (or expat) is the real fix. Never run two ESPHome builds at once.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ A media card like a phone's "now playing", with the album cover on a Guition.
|
|||||||
- **The media card is new.** Tap a media player's tile and the card shows the album cover, the title, the artist and the album, a progress bar that runs while the track plays with the elapsed and total time at its ends, three round keys (previous, play or pause on the blue key, next) and a volume row: a mute key, a wide slider with a round knob, and the percentage. The keys the player does not offer are faded, the mute key and the slider follow what Home Assistant reports (a muted player shows a grey fill and *Muted*), and a player that is off shows one power key. A stream without a length shows no bar and no times. It works in the light and the dark look.
|
- **The media card is new.** Tap a media player's tile and the card shows the album cover, the title, the artist and the album, a progress bar that runs while the track plays with the elapsed and total time at its ends, three round keys (previous, play or pause on the blue key, next) and a volume row: a mute key, a wide slider with a round knob, and the percentage. The keys the player does not offer are faded, the mute key and the slider follow what Home Assistant reports (a muted player shows a grey fill and *Muted*), and a player that is off shows one power key. A stream without a length shows no bar and no times. It works in the light and the dark look.
|
||||||
- **The album cover** comes the way camera images do (app 0.2.66): the screen asks ESP Screen Manager, the app fetches the picture from Home Assistant with its own access, makes it exactly the size the card asks for with the corners already rounded over the card's colour, and serves it on port 8098. A Guition loads it once and keeps it until the picture changes, the card closes or the page turns. The CYD has no memory for pictures and shows the player's icon in the cover's place, as it does when a player has no picture (a radio station, a player that is off).
|
- **The album cover** comes the way camera images do (app 0.2.66): the screen asks ESP Screen Manager, the app fetches the picture from Home Assistant with its own access, makes it exactly the size the card asks for with the corners already rounded over the card's colour, and serves it on port 8098. A Guition loads it once and keeps it until the picture changes, the card closes or the page turns. The CYD has no memory for pictures and shows the player's icon in the cover's place, as it does when a player has no picture (a radio station, a player that is off).
|
||||||
- **A media tile over the whole page** (size *Full page*) is the same card under the tile's head: the cover at the left, the track, the bar and the keys beside it, the volume row along the bottom. The keys and the slider work on the tile; holding the tile still opens the card.
|
- **A media tile over the whole page** (size *Full page*) is the same card under the tile's head: the cover at the left, the track, the bar and the keys beside it, the volume row along the bottom. The keys and the slider work on the tile; holding the tile still opens the card.
|
||||||
|
- **One picture at a time.** A download shares the screen's loop with touch and drawing, so a Guition never loads two pictures at once. An alert closes an open card, and the card's cover goes with it. A media tile over the whole page stays under the alert, and there the alert's picture goes first: a cover that has not started waits until the alert's picture is there, and a cover already on its way finishes before the alert's picture starts. A camera full screen and the cover share one picture, so they never load together. Memory is not the limit: the pictures live in PSRAM (a cover about 70 KB, the alert's 172 KB).
|
||||||
- **The card's clock runs on the screen.** Home Assistant reports where a track was and when; the bar and the elapsed time run on from there every second without any traffic, and stand still while the track is paused.
|
- **The card's clock runs on the screen.** Home Assistant reports where a track was and when; the bar and the elapsed time run on from there every second without any traffic, and stand still while the track is paused.
|
||||||
- Claude in Home Assistant and the editor need nothing new: a `media_player` tile is the same tile, with the new card behind it.
|
- Claude in Home Assistant and the editor need nothing new: a `media_player` tile is the same tile, with the new card behind it.
|
||||||
- Needs firmware 0.2.64 for the new card; the app serves covers only to a Guition on that firmware. Older firmware keeps the old card. Includes everything from 0.2.76.
|
- Needs firmware 0.2.64 for the new card; the app serves covers only to a Guition on that firmware. Older firmware keeps the old card. Includes everything from 0.2.76.
|
||||||
|
|||||||
Reference in New Issue
Block a user