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)
|
- 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
|
||||||
|
|||||||
@@ -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 ===");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user