Automount und unmount implementiert, Darstellung verbessert

This commit is contained in:
2026-10-03 03:39:56 +02:00
parent 5da29cf3ca
commit ce09b5da30
2 changed files with 189 additions and 15 deletions

View File

@@ -12,6 +12,10 @@ Display + 3 Taster, Oberfläche mit LVGL.
- 160×128-TFT-Display mit LVGL-Oberfläche (Liste, Statuszeile, Aktivitätsanzeige) - 160×128-TFT-Display mit LVGL-Oberfläche (Liste, Statuszeile, Aktivitätsanzeige)
- Alphabetisch sortierte Image-Liste - Alphabetisch sortierte Image-Liste
- Manuelles Neuladen der Liste per Tastenkombination (z.B. nach SD-Kartenwechsel) - Manuelles Neuladen der Liste per Tastenkombination (z.B. nach SD-Kartenwechsel)
- Auswerfen: erneutes Drücken auf das gemountete Image wirft es aus
(der Host sieht ein leeres Laufwerk, ohne dass man den Floppinator abziehen muss)
- Autostart-Image: ein Image mit `-` vor dem Dateinamen ist direkt nach dem
Einschalten gemountet
- Aktivitäts-LED (Onboard-LED) leuchtet bei Lese-/Schreibzugriffen - Aktivitäts-LED (Onboard-LED) leuchtet bei Lese-/Schreibzugriffen
- Debug-Ausgaben über USB-Seriell, zentral abschaltbar - Debug-Ausgaben über USB-Seriell, zentral abschaltbar
@@ -160,6 +164,7 @@ GRAM und sichtbarem Glas). Falls das auftritt: Werte wie `(0, 0)`,
|-----------------------------------|------------------------------| |-----------------------------------|------------------------------|
| In der Liste navigieren | Zurück / Weiter | | In der Liste navigieren | Zurück / Weiter |
| Fokussiertes Image mounten | Auswahl | | Fokussiertes Image mounten | Auswahl |
| Gemountetes Image auswerfen | Auswahl auf dem bereits gemounteten (grün markierten) Image |
| Image-Liste neu laden (z.B. nach SD-Kartenwechsel) | Zurück + Weiter gleichzeitig | | Image-Liste neu laden (z.B. nach SD-Kartenwechsel) | Zurück + Weiter gleichzeitig |
Das SD-Modul hat keinen Card-Detect-Pin — ein Kartenwechsel im laufenden Das SD-Modul hat keinen Card-Detect-Pin — ein Kartenwechsel im laufenden
@@ -168,6 +173,44 @@ automatischer Erkennung per periodischem Polling haben sich als
unzuverlässig erwiesen). Nach einem Kartenwechsel einfach kurz unzuverlässig erwiesen). Nach einem Kartenwechsel einfach kurz
Zurück+Weiter drücken. Zurück+Weiter drücken.
### Auswerfen
Drückt man **Auswahl** auf dem Image, das gerade gemountet ist (in der
Liste mit grünem Balken links markiert, in der Statuszeile mit Haken), wird es ausgeworfen.
Der Host sieht dann ein Laufwerk ohne Medium, als hätte man die Diskette
gezogen. Das ist z.B. am Ende der MS-DOS-Installation nützlich, wenn
verlangt wird, alle Disketten zu entfernen. Ein anderes Image mountet man
weiterhin direkt per Auswahl, ein vorheriges Auswerfen ist dafür nicht nötig.
Das Laufwerk bleibt leer, bis wieder ein Image gemountet wird oder der
Floppinator neu startet (dann greift ggf. das Autostart-Image, siehe unten).
### Autostart-Image
Ein Image kann so markiert werden, dass es direkt nach dem Einschalten
gemountet ist, und zwar schon bevor der Host das Laufwerk zum ersten Mal
sieht. Das hilft, wenn der Host sofort von Diskette booten will und keine
Zeit bleibt, von Hand ein Image zu wählen.
Die Markierung ist ein **`-` vor dem Dateinamen**:
```
-msdos622-boot.img -> Autostart-Image, erscheint in der Liste als "msdos622-boot.img"
msdos622-boot.img -> normales Image
```
- In der Liste und in der Statuszeile wird der Name ohne `-` angezeigt,
das Autostart-Image trägt ein Power-Symbol statt des Datei-Symbols. Die
Sortierung bleibt alphabetisch nach dem Namen ohne `-`.
- Markieren und Entfernen der Markierung geschieht durch Umbenennen der
Datei am PC. In der Shell braucht es dafür `--`, weil der Name sonst wie
eine Option aussieht: `mv -- msdos622-boot.img -msdos622-boot.img`
- Sind mehrere Images markiert, wird das alphabetisch erste gemountet.
- Der Autostart greift nur beim Einschalten bzw. Neustart. Nach dem
Neuladen der Liste per Zurück+Weiter startet der Floppinator wie bisher
ohne Medium.
- Der Dateiname darf inklusive `-` höchstens 31 Zeichen lang sein.
## Bekannte Einschränkungen ## Bekannte Einschränkungen
- Kein automatisches Remounten des zuletzt gewählten Images nach einem - Kein automatisches Remounten des zuletzt gewählten Images nach einem

View File

@@ -54,7 +54,7 @@
#define ACTIVITY_LED_PIN LED_BUILTIN #define ACTIVITY_LED_PIN LED_BUILTIN
#define DEBUG_ENABLED true #define DEBUG_ENABLED false
#if DEBUG_ENABLED #if DEBUG_ENABLED
#define DBG_PRINT(...) do { if (Serial) { Serial.print(__VA_ARGS__); } } while (0) #define DBG_PRINT(...) do { if (Serial) { Serial.print(__VA_ARGS__); } } while (0)
@@ -64,7 +64,8 @@
#define DBG_PRINTLN(...) do {} while (0) #define DBG_PRINTLN(...) do {} while (0)
#endif #endif
#define MAX_IMAGES 16 #define MAX_IMAGES 64
#define AUTOSTART_PREFIX '-' // Dateiname beginnt mit '-' = Autostart-Image
#define SCREEN_W 160 #define SCREEN_W 160
#define SCREEN_H 128 #define SCREEN_H 128
@@ -128,6 +129,21 @@ bool buttonPressed(Button &b)
return false; return false;
} }
// --- Autostart-Markierung ---
// Ein fuehrendes '-' im Dateinamen markiert das Image, das direkt nach dem
// Einschalten gemountet wird. In der Oberflaeche und beim Sortieren wird der
// Name ohne '-' verwendet, geoeffnet wird die Datei mit ihrem echten Namen.
inline bool isAutostartName(const char *name)
{
return name[0] == AUTOSTART_PREFIX;
}
inline const char *displayName(const char *name)
{
return isAutostartName(name) ? name + 1 : name;
}
// --- LVGL: Display-Flush + Tasten-Eingabegeraet --- // --- LVGL: Display-Flush + Tasten-Eingabegeraet ---
static lv_display_t *lvDisplay; static lv_display_t *lvDisplay;
@@ -283,6 +299,14 @@ void buildUi()
lv_obj_set_style_border_width(imageList, 0, 0); lv_obj_set_style_border_width(imageList, 0, 0);
lv_obj_set_style_pad_all(imageList, 2, 0); lv_obj_set_style_pad_all(imageList, 2, 0);
lv_obj_set_style_pad_row(imageList, 2, 0); lv_obj_set_style_pad_row(imageList, 2, 0);
// Flache Liste ohne Theme-Rundungen/Schatten; schmale Scrollleiste
lv_obj_set_style_radius(imageList, 0, 0);
lv_obj_set_style_bg_opa(imageList, LV_OPA_COVER, 0);
lv_obj_set_style_shadow_width(imageList, 0, 0);
lv_obj_set_style_outline_width(imageList, 0, 0);
lv_obj_set_style_width(imageList, 3, LV_PART_SCROLLBAR);
lv_obj_set_style_bg_color(imageList, lv_color_hex(0x606060), LV_PART_SCROLLBAR);
lv_obj_set_style_bg_opa(imageList, LV_OPA_COVER, LV_PART_SCROLLBAR);
// Fusszeile // Fusszeile
lv_obj_t *footer = lv_obj_create(scr); lv_obj_t *footer = lv_obj_create(scr);
@@ -312,7 +336,11 @@ void listButtonEventCb(lv_event_t *e)
lv_obj_t *btn = (lv_obj_t *)lv_event_get_target(e); lv_obj_t *btn = (lv_obj_t *)lv_event_get_target(e);
int idx = (int)(uintptr_t)lv_obj_get_user_data(btn); int idx = (int)(uintptr_t)lv_obj_get_user_data(btn);
if (idx != mountedIndex) if (idx == mountedIndex)
{
ejectImage(); // erneutes Druecken auf das gemountete Image = auswerfen
}
else
{ {
mountImage(idx); mountImage(idx);
} }
@@ -324,7 +352,7 @@ void refreshStatusBar()
if (mountedIndex >= 0) if (mountedIndex >= 0)
{ {
snprintf(buf, sizeof(buf), LV_SYMBOL_OK " %s", images[mountedIndex].name); snprintf(buf, sizeof(buf), LV_SYMBOL_OK " %s", displayName(images[mountedIndex].name));
} }
else else
{ {
@@ -348,9 +376,45 @@ void refreshStatusBar()
lv_obj_set_style_bg_color(listButtons[i], lv_color_hex(0x202020), 0); lv_obj_set_style_bg_color(listButtons[i], lv_color_hex(0x202020), 0);
lv_obj_set_style_bg_opa(listButtons[i], LV_OPA_COVER, 0); lv_obj_set_style_bg_opa(listButtons[i], LV_OPA_COVER, 0);
} }
// Gruener Balken links markiert das gemountete Image, auch wenn es fokussiert (blau) ist
lv_coord_t barW = ((int8_t)i == mountedIndex) ? 3 : 0;
lv_obj_set_style_border_color(listButtons[i], lv_color_hex(0x2ECC71), 0);
lv_obj_set_style_border_color(listButtons[i], lv_color_hex(0x2ECC71), LV_STATE_FOCUSED);
lv_obj_set_style_border_opa(listButtons[i], LV_OPA_COVER, 0);
lv_obj_set_style_border_opa(listButtons[i], LV_OPA_COVER, LV_STATE_FOCUSED);
lv_obj_set_style_border_width(listButtons[i], barW, 0);
lv_obj_set_style_border_width(listButtons[i], barW, LV_STATE_FOCUSED);
} }
} }
// Einheitlicher Look der Listenzeilen: kompakt (16 px Text + 2x5 px Innenabstand
// = 26 px, drei Zeilen fuellen die 86 px der Liste exakt), Fokus blau,
// kein Theme-Outline. Der gruene Balken fuer "gemountet" kommt in refreshStatusBar().
void styleListButton(lv_obj_t *btn)
{
lv_obj_set_style_radius(btn, 3, 0);
lv_obj_set_style_pad_ver(btn, 5, 0);
lv_obj_set_style_pad_hor(btn, 6, 0);
lv_obj_set_style_pad_column(btn, 6, 0);
lv_obj_set_style_shadow_width(btn, 0, 0);
lv_obj_set_style_text_color(btn, lv_color_hex(0xDDDDDD), 0);
lv_obj_set_style_bg_color(btn, lv_color_hex(0x2A5DB0), LV_STATE_FOCUSED);
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, LV_STATE_FOCUSED);
lv_obj_set_style_text_color(btn, lv_color_hex(0xFFFFFF), LV_STATE_FOCUSED);
lv_obj_set_style_outline_width(btn, 0, 0);
lv_obj_set_style_outline_width(btn, 0, LV_STATE_FOCUSED);
lv_obj_set_style_outline_width(btn, 0, LV_STATE_FOCUS_KEY);
// Nur der linke Rand wird je nach Mount-Status benutzt
lv_obj_set_style_border_side(btn, LV_BORDER_SIDE_LEFT, 0);
lv_obj_set_style_border_side(btn, LV_BORDER_SIDE_LEFT, LV_STATE_FOCUSED);
lv_obj_set_style_border_width(btn, 0, 0);
lv_obj_set_style_border_width(btn, 0, LV_STATE_FOCUSED);
}
void rebuildImageList() void rebuildImageList()
{ {
lv_obj_clean(imageList); lv_obj_clean(imageList);
@@ -361,13 +425,16 @@ void rebuildImageList()
if (imageCount == 0) if (imageCount == 0)
{ {
lv_list_add_text(imageList, "Keine Images gefunden"); lv_obj_t *hint = lv_list_add_text(imageList, "Keine Images gefunden");
lv_obj_set_style_text_color(hint, lv_color_hex(0x888888), 0);
return; return;
} }
for (uint8_t i = 0; i < imageCount; i++) for (uint8_t i = 0; i < imageCount; i++)
{ {
lv_obj_t *btn = lv_list_add_btn(imageList, LV_SYMBOL_FILE, images[i].name); const char *icon = isAutostartName(images[i].name) ? LV_SYMBOL_POWER : LV_SYMBOL_FILE;
lv_obj_t *btn = lv_list_add_btn(imageList, icon, displayName(images[i].name));
styleListButton(btn);
lv_obj_set_user_data(btn, (void *)(uintptr_t)i); lv_obj_set_user_data(btn, (void *)(uintptr_t)i);
lv_obj_add_event_cb(btn, listButtonEventCb, LV_EVENT_CLICKED, NULL); lv_obj_add_event_cb(btn, listButtonEventCb, LV_EVENT_CLICKED, NULL);
lv_group_add_obj(lvGroup, btn); lv_group_add_obj(lvGroup, btn);
@@ -437,7 +504,7 @@ void sortImages()
FloppyImage key = images[i]; FloppyImage key = images[i];
int8_t j = i - 1; int8_t j = i - 1;
while (j >= 0 && strcasecmp(images[j].name, key.name) > 0) while (j >= 0 && strcasecmp(displayName(images[j].name), displayName(key.name)) > 0)
{ {
images[j + 1] = images[j]; images[j + 1] = images[j];
j--; j--;
@@ -480,11 +547,14 @@ void rescanSdCard()
refreshStatusBar(); refreshStatusBar();
} }
void mountImage(int8_t idx) // Oeffnet das Image und meldet es dem USB-MSC als Medium (ohne USB-Replug).
// Wird direkt von setup() fuer das Autostart-Image benutzt, damit der Host
// schon beim ersten Enumerieren ein Medium sieht.
bool openImage(int8_t idx)
{ {
if (idx < 0 || idx >= imageCount) if (idx < 0 || idx >= imageCount)
{ {
return; return false;
} }
String fname = String(images[idx].name); String fname = String(images[idx].name);
@@ -497,10 +567,6 @@ void mountImage(int8_t idx)
DBG_PRINT(fname); DBG_PRINT(fname);
DBG_PRINTLN("'"); DBG_PRINTLN("'");
usb_msc.setUnitReady(false);
TinyUSBDevice.detach();
delay(200);
if (currentFile) if (currentFile)
{ {
currentFile.close(); currentFile.close();
@@ -514,8 +580,8 @@ void mountImage(int8_t idx)
if (!ok) if (!ok)
{ {
TinyUSBDevice.attach(); mountedIndex = -1;
return; return false;
} }
currentFilename = fname; currentFilename = fname;
@@ -527,12 +593,67 @@ void mountImage(int8_t idx)
usb_msc.setCapacity(numSectors, 512); usb_msc.setCapacity(numSectors, 512);
usb_msc.setUnitReady(true); usb_msc.setUnitReady(true);
return true;
}
void mountImage(int8_t idx)
{
if (idx < 0 || idx >= imageCount)
{
return;
}
usb_msc.setUnitReady(false);
TinyUSBDevice.detach();
delay(200);
openImage(idx);
TinyUSBDevice.attach(); TinyUSBDevice.attach();
refreshStatusBar(); refreshStatusBar();
} }
// Wirft das gemountete Image aus: der Host sieht ein Laufwerk ohne Medium,
// so als haette man die Diskette gezogen (gleiche Replug-Mechanik wie beim Mounten).
void ejectImage()
{
if (mountedIndex < 0)
{
return;
}
DBG_PRINTLN("Auswerfen...");
usb_msc.setUnitReady(false);
TinyUSBDevice.detach();
delay(200);
if (currentFile)
{
currentFile.close();
}
currentFilename = "";
mountedIndex = -1;
TinyUSBDevice.attach();
refreshStatusBar();
}
// Index des Autostart-Images (erstes markiertes in sortierter Reihenfolge) oder -1
int8_t findAutostartImage()
{
for (uint8_t i = 0; i < imageCount; i++)
{
if (isAutostartName(images[i].name))
{
return (int8_t)i;
}
}
return -1;
}
void setup() void setup()
{ {
Serial.begin(115200); Serial.begin(115200);
@@ -604,6 +725,16 @@ void setup()
usb_msc.setReadWriteCallback(msc_read_cb, msc_write_cb, msc_flush_cb); usb_msc.setReadWriteCallback(msc_read_cb, msc_write_cb, msc_flush_cb);
usb_msc.setCapacity(2880, 512); usb_msc.setCapacity(2880, 512);
usb_msc.setUnitReady(false); usb_msc.setUnitReady(false);
// Autostart-Image (Dateiname mit '-') schon vor dem ersten Enumerieren
// einlegen, damit der Host direkt beim Einschalten ein Medium sieht.
int8_t autostartIdx = findAutostartImage();
if (autostartIdx >= 0 && openImage(autostartIdx))
{
lv_group_focus_obj(listButtons[autostartIdx]);
refreshStatusBar();
}
usb_msc.begin(); usb_msc.begin();
DBG_PRINTLN("=== Setup abgeschlossen ==="); DBG_PRINTLN("=== Setup abgeschlossen ===");
} }