Automount und unmount implementiert, Darstellung verbessert
This commit is contained in:
43
README.md
43
README.md
@@ -12,6 +12,10 @@ Display + 3 Taster, Oberfläche mit LVGL.
|
||||
- 160×128-TFT-Display mit LVGL-Oberfläche (Liste, Statuszeile, Aktivitätsanzeige)
|
||||
- Alphabetisch sortierte Image-Liste
|
||||
- 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
|
||||
- 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 |
|
||||
| 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 |
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
- Kein automatisches Remounten des zuletzt gewählten Images nach einem
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
#define ACTIVITY_LED_PIN LED_BUILTIN
|
||||
|
||||
#define DEBUG_ENABLED true
|
||||
#define DEBUG_ENABLED false
|
||||
|
||||
#if DEBUG_ENABLED
|
||||
#define DBG_PRINT(...) do { if (Serial) { Serial.print(__VA_ARGS__); } } while (0)
|
||||
@@ -64,7 +64,8 @@
|
||||
#define DBG_PRINTLN(...) do {} while (0)
|
||||
#endif
|
||||
|
||||
#define MAX_IMAGES 16
|
||||
#define MAX_IMAGES 64
|
||||
#define AUTOSTART_PREFIX '-' // Dateiname beginnt mit '-' = Autostart-Image
|
||||
#define SCREEN_W 160
|
||||
#define SCREEN_H 128
|
||||
|
||||
@@ -128,6 +129,21 @@ bool buttonPressed(Button &b)
|
||||
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 ---
|
||||
|
||||
static lv_display_t *lvDisplay;
|
||||
@@ -283,6 +299,14 @@ void buildUi()
|
||||
lv_obj_set_style_border_width(imageList, 0, 0);
|
||||
lv_obj_set_style_pad_all(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
|
||||
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);
|
||||
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);
|
||||
}
|
||||
@@ -324,7 +352,7 @@ void refreshStatusBar()
|
||||
|
||||
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
|
||||
{
|
||||
@@ -348,9 +376,45 @@ void refreshStatusBar()
|
||||
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);
|
||||
}
|
||||
|
||||
// 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()
|
||||
{
|
||||
lv_obj_clean(imageList);
|
||||
@@ -361,13 +425,16 @@ void rebuildImageList()
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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_add_event_cb(btn, listButtonEventCb, LV_EVENT_CLICKED, NULL);
|
||||
lv_group_add_obj(lvGroup, btn);
|
||||
@@ -437,7 +504,7 @@ void sortImages()
|
||||
FloppyImage key = images[i];
|
||||
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];
|
||||
j--;
|
||||
@@ -480,11 +547,14 @@ void rescanSdCard()
|
||||
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)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
String fname = String(images[idx].name);
|
||||
@@ -497,10 +567,6 @@ void mountImage(int8_t idx)
|
||||
DBG_PRINT(fname);
|
||||
DBG_PRINTLN("'");
|
||||
|
||||
usb_msc.setUnitReady(false);
|
||||
TinyUSBDevice.detach();
|
||||
delay(200);
|
||||
|
||||
if (currentFile)
|
||||
{
|
||||
currentFile.close();
|
||||
@@ -514,8 +580,8 @@ void mountImage(int8_t idx)
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
TinyUSBDevice.attach();
|
||||
return;
|
||||
mountedIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
currentFilename = fname;
|
||||
@@ -527,12 +593,67 @@ void mountImage(int8_t idx)
|
||||
|
||||
usb_msc.setCapacity(numSectors, 512);
|
||||
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();
|
||||
|
||||
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()
|
||||
{
|
||||
Serial.begin(115200);
|
||||
@@ -604,6 +725,16 @@ void setup()
|
||||
usb_msc.setReadWriteCallback(msc_read_cb, msc_write_cb, msc_flush_cb);
|
||||
usb_msc.setCapacity(2880, 512);
|
||||
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();
|
||||
DBG_PRINTLN("=== Setup abgeschlossen ===");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user