esp32tool 1.1.9 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.nojekyll +0 -0
  2. package/README.md +100 -6
  3. package/apple-touch-icon.png +0 -0
  4. package/build-electron-cli.cjs +177 -0
  5. package/build-single-binary.cjs +295 -0
  6. package/css/light.css +11 -0
  7. package/css/style.css +261 -41
  8. package/dist/cli.d.ts +17 -0
  9. package/dist/cli.js +458 -0
  10. package/dist/console.d.ts +15 -0
  11. package/dist/console.js +237 -0
  12. package/dist/const.d.ts +99 -0
  13. package/dist/const.js +129 -8
  14. package/dist/esp_loader.d.ts +244 -22
  15. package/dist/esp_loader.js +1960 -251
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.js +37 -4
  18. package/dist/node-usb-adapter.d.ts +47 -0
  19. package/dist/node-usb-adapter.js +725 -0
  20. package/dist/stubs/index.d.ts +1 -2
  21. package/dist/stubs/index.js +4 -0
  22. package/dist/util/console-color.d.ts +19 -0
  23. package/dist/util/console-color.js +272 -0
  24. package/dist/util/line-break-transformer.d.ts +5 -0
  25. package/dist/util/line-break-transformer.js +17 -0
  26. package/dist/web/index.js +1 -1
  27. package/electron/cli-main.cjs +74 -0
  28. package/electron/main.cjs +338 -0
  29. package/electron/main.js +7 -2
  30. package/favicon.ico +0 -0
  31. package/fix-cli-imports.cjs +127 -0
  32. package/generate-icons.sh +89 -0
  33. package/icons/icon-128.png +0 -0
  34. package/icons/icon-144.png +0 -0
  35. package/icons/icon-152.png +0 -0
  36. package/icons/icon-192.png +0 -0
  37. package/icons/icon-384.png +0 -0
  38. package/icons/icon-512.png +0 -0
  39. package/icons/icon-72.png +0 -0
  40. package/icons/icon-96.png +0 -0
  41. package/index.html +143 -73
  42. package/install-android.html +411 -0
  43. package/js/console.js +269 -0
  44. package/js/modules/esptool.js +1 -1
  45. package/js/script.js +750 -175
  46. package/js/util/console-color.js +282 -0
  47. package/js/util/line-break-transformer.js +19 -0
  48. package/js/webusb-serial.js +1017 -0
  49. package/license.md +1 -1
  50. package/manifest.json +89 -0
  51. package/package.cli.json +29 -0
  52. package/package.json +35 -24
  53. package/screenshots/desktop.png +0 -0
  54. package/screenshots/mobile.png +0 -0
  55. package/src/cli.ts +618 -0
  56. package/src/console.ts +278 -0
  57. package/src/const.ts +165 -8
  58. package/src/esp_loader.ts +2354 -302
  59. package/src/index.ts +69 -3
  60. package/src/node-usb-adapter.ts +924 -0
  61. package/src/stubs/index.ts +4 -1
  62. package/src/util/console-color.ts +290 -0
  63. package/src/util/line-break-transformer.ts +20 -0
  64. package/sw.js +155 -0
@@ -0,0 +1,338 @@
1
+ const { app, BrowserWindow, Menu, session, ipcMain, dialog } = require('electron');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ // Handle creating/removing shortcuts on Windows when installing/uninstalling.
6
+ // Only required for Windows Squirrel installer
7
+ if (process.platform === 'win32') {
8
+ try {
9
+ if (require('electron-squirrel-startup')) {
10
+ app.quit();
11
+ }
12
+ } catch (e) {
13
+ // Module not available, ignore
14
+ }
15
+ }
16
+
17
+ let mainWindow;
18
+
19
+ // Store granted serial port devices
20
+ const grantedDevices = new Map();
21
+
22
+ function createWindow() {
23
+ mainWindow = new BrowserWindow({
24
+ width: 1200,
25
+ height: 800,
26
+ minWidth: 800,
27
+ minHeight: 600,
28
+ webPreferences: {
29
+ nodeIntegration: false,
30
+ contextIsolation: true,
31
+ preload: path.join(__dirname, 'preload.js'),
32
+ },
33
+ icon: path.join(__dirname, 'icons', 'icon.png'),
34
+ title: 'ESP32Tool',
35
+ autoHideMenuBar: false,
36
+ });
37
+
38
+ // Load the index.html of the app
39
+ mainWindow.loadFile(path.join(__dirname, '..', 'index.html'));
40
+
41
+ // Open DevTools in development
42
+ if (process.env.NODE_ENV === 'development') {
43
+ mainWindow.webContents.openDevTools();
44
+ }
45
+
46
+ mainWindow.on('closed', () => {
47
+ mainWindow = null;
48
+ });
49
+
50
+ // Setup serial port handlers for this window's session
51
+ setupSerialPortHandlers(mainWindow.webContents.session);
52
+ }
53
+
54
+ function setupSerialPortHandlers(ses) {
55
+ let lastSelectedPort = null;
56
+ let esp32s2ReconnectPending = false;
57
+ let portSelectionQueue = [];
58
+
59
+ // Handle serial port selection - shows when navigator.serial.requestPort() is called
60
+ ses.on('select-serial-port', (event, portList, webContents, callback) => {
61
+ event.preventDefault();
62
+
63
+ console.log('Available serial ports:', portList.map(p => ({
64
+ portId: p.portId,
65
+ portName: p.portName,
66
+ displayName: p.displayName
67
+ })));
68
+
69
+ if (portList && portList.length > 0) {
70
+ // Try to find ESP-compatible port
71
+ const espPort = portList.find(port => {
72
+ const name = (port.displayName || port.portName || '').toLowerCase();
73
+ return name.includes('cp2102') ||
74
+ name.includes('cp2103') ||
75
+ name.includes('cp2104') ||
76
+ name.includes('cp2105') ||
77
+ name.includes('cp2108') ||
78
+ name.includes('ch9102') ||
79
+ name.includes('ch9104') ||
80
+ name.includes('ch340') ||
81
+ name.includes('ch341') ||
82
+ name.includes('ch343') ||
83
+ name.includes('ftdi') ||
84
+ name.includes('usb') ||
85
+ name.includes('uart') ||
86
+ name.includes('silicon labs') ||
87
+ name.includes('esp');
88
+ });
89
+
90
+ // Select ESP-compatible port or first available
91
+ const selectedPort = espPort || portList[0];
92
+ console.log('Selected port:', selectedPort.portId, selectedPort.displayName || selectedPort.portName);
93
+ lastSelectedPort = selectedPort;
94
+
95
+ callback(selectedPort.portId);
96
+ } else {
97
+ console.log('No serial ports available - queuing selection');
98
+ // No ports available yet - queue this callback for when a port appears
99
+ portSelectionQueue.push(callback);
100
+ }
101
+ });
102
+
103
+ // Track port additions - handle ESP32-S2 reconnect
104
+ ses.on('serial-port-added', (event, port) => {
105
+ console.log('Serial port added:', port);
106
+
107
+ // If we have queued port selections, handle them now
108
+ if (portSelectionQueue.length > 0) {
109
+ console.log('Processing queued port selection');
110
+ const callback = portSelectionQueue.shift();
111
+ callback(port.portId);
112
+ lastSelectedPort = port;
113
+ }
114
+
115
+ // Check if this looks like an ESP32-S2 CDC port appearing after ROM port disappeared
116
+ if (lastSelectedPort && port.portName !== lastSelectedPort.portName) {
117
+ const name = (port.displayName || port.portName || '').toLowerCase();
118
+ if (name.includes('esp') || name.includes('usb') || name.includes('uart')) {
119
+ console.log('ESP32-S2 reconnect detected - new CDC port available');
120
+ esp32s2ReconnectPending = true;
121
+ }
122
+ }
123
+ });
124
+
125
+ // Track port removals - detect ESP32-S2 disconnect
126
+ ses.on('serial-port-removed', (event, port) => {
127
+ console.log('Serial port removed:', port);
128
+
129
+ // If the last selected port was removed, prepare for reconnect
130
+ if (lastSelectedPort && port.portId === lastSelectedPort.portId) {
131
+ console.log('Last selected port removed - may be ESP32-S2 mode switch');
132
+ // Don't clear lastSelectedPort yet, we might need it for comparison
133
+ }
134
+ });
135
+
136
+ // Grant permission for serial port access checks
137
+ ses.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
138
+ if (permission === 'serial') {
139
+ return true;
140
+ }
141
+ return true;
142
+ });
143
+
144
+ // Handle device permission requests
145
+ ses.setDevicePermissionHandler((details) => {
146
+ if (details.deviceType === 'serial') {
147
+ if (details.device) {
148
+ grantedDevices.set(details.device.deviceId, details.device);
149
+ }
150
+ return true;
151
+ }
152
+ return true;
153
+ });
154
+ }
155
+
156
+ // Create application menu
157
+ function createMenu() {
158
+ const template = [
159
+ {
160
+ label: 'File',
161
+ submenu: [
162
+ { role: 'quit' }
163
+ ]
164
+ },
165
+ {
166
+ label: 'Edit',
167
+ submenu: [
168
+ { role: 'undo' },
169
+ { role: 'redo' },
170
+ { type: 'separator' },
171
+ { role: 'cut' },
172
+ { role: 'copy' },
173
+ { role: 'paste' },
174
+ { role: 'selectAll' }
175
+ ]
176
+ },
177
+ {
178
+ label: 'View',
179
+ submenu: [
180
+ { role: 'reload' },
181
+ { role: 'forceReload' },
182
+ { role: 'toggleDevTools' },
183
+ { type: 'separator' },
184
+ { role: 'resetZoom' },
185
+ { role: 'zoomIn' },
186
+ { role: 'zoomOut' },
187
+ { type: 'separator' },
188
+ { role: 'togglefullscreen' }
189
+ ]
190
+ },
191
+ {
192
+ label: 'Help',
193
+ submenu: [
194
+ {
195
+ label: 'About ESP32Tool',
196
+ click: async () => {
197
+ const { shell } = require('electron');
198
+ await shell.openExternal('https://github.com/Jason2866/esp32tool');
199
+ }
200
+ },
201
+ {
202
+ label: 'ESP32 Documentation',
203
+ click: async () => {
204
+ const { shell } = require('electron');
205
+ await shell.openExternal('https://docs.espressif.com/');
206
+ }
207
+ }
208
+ ]
209
+ }
210
+ ];
211
+
212
+ // macOS specific menu adjustments
213
+ if (process.platform === 'darwin') {
214
+ template.unshift({
215
+ label: app.getName(),
216
+ submenu: [
217
+ { role: 'about' },
218
+ { type: 'separator' },
219
+ { role: 'services' },
220
+ { type: 'separator' },
221
+ { role: 'hide' },
222
+ { role: 'hideOthers' },
223
+ { role: 'unhide' },
224
+ { type: 'separator' },
225
+ { role: 'quit' }
226
+ ]
227
+ });
228
+ }
229
+
230
+ const menu = Menu.buildFromTemplate(template);
231
+ Menu.setApplicationMenu(menu);
232
+ }
233
+
234
+ // This method will be called when Electron has finished initialization
235
+ app.whenReady().then(() => {
236
+ createMenu();
237
+ createWindow();
238
+
239
+ app.on('activate', () => {
240
+ // On macOS re-create a window when dock icon is clicked and no windows are open
241
+ if (BrowserWindow.getAllWindows().length === 0) {
242
+ createWindow();
243
+ }
244
+ });
245
+ });
246
+
247
+ // Quit when all windows are closed, except on macOS
248
+ app.on('window-all-closed', () => {
249
+ if (process.platform !== 'darwin') {
250
+ app.quit();
251
+ }
252
+ });
253
+
254
+ // ============================================
255
+ // IPC Handlers for File Operations
256
+ // ============================================
257
+
258
+ // Save file dialog and write data
259
+ ipcMain.handle('save-file', async (event, { data, defaultFilename, filters }) => {
260
+ const result = await dialog.showSaveDialog(mainWindow, {
261
+ defaultPath: defaultFilename,
262
+ filters: filters || [
263
+ { name: 'Binary Files', extensions: ['bin'] },
264
+ { name: 'All Files', extensions: ['*'] }
265
+ ]
266
+ });
267
+
268
+ if (result.canceled || !result.filePath) {
269
+ return { success: false, canceled: true };
270
+ }
271
+
272
+ try {
273
+ // Convert data to Buffer if it's a Uint8Array
274
+ const buffer = Buffer.from(data);
275
+ fs.writeFileSync(result.filePath, buffer);
276
+ return { success: true, filePath: result.filePath };
277
+ } catch (error) {
278
+ return { success: false, error: error.message };
279
+ }
280
+ });
281
+
282
+ // Open file dialog and read data
283
+ ipcMain.handle('open-file', async (event, { filters }) => {
284
+ const result = await dialog.showOpenDialog(mainWindow, {
285
+ properties: ['openFile'],
286
+ filters: filters || [
287
+ { name: 'Binary Files', extensions: ['bin'] },
288
+ { name: 'All Files', extensions: ['*'] }
289
+ ]
290
+ });
291
+
292
+ if (result.canceled || result.filePaths.length === 0) {
293
+ return { success: false, canceled: true };
294
+ }
295
+
296
+ try {
297
+ const filePath = result.filePaths[0];
298
+ const data = fs.readFileSync(filePath);
299
+ const filename = path.basename(filePath);
300
+ return {
301
+ success: true,
302
+ filePath,
303
+ filename,
304
+ data: Array.from(data) // Convert Buffer to array for IPC transfer
305
+ };
306
+ } catch (error) {
307
+ return { success: false, error: error.message };
308
+ }
309
+ });
310
+
311
+ // Show input dialog (replacement for prompt())
312
+ ipcMain.handle('show-prompt', async (event, { message, defaultValue }) => {
313
+ // Use a simple approach - return the default value
314
+ // In Electron, we use the save dialog instead of prompt
315
+ return defaultValue;
316
+ });
317
+
318
+ // Show message box
319
+ ipcMain.handle('show-message', async (event, { type, title, message, buttons }) => {
320
+ const result = await dialog.showMessageBox(mainWindow, {
321
+ type: type || 'info',
322
+ title: title || 'ESP32Tool',
323
+ message: message,
324
+ buttons: buttons || ['OK']
325
+ });
326
+ return result.response;
327
+ });
328
+
329
+ // Show confirm dialog
330
+ ipcMain.handle('show-confirm', async (event, { message }) => {
331
+ const result = await dialog.showMessageBox(mainWindow, {
332
+ type: 'question',
333
+ title: 'Confirm',
334
+ message: message,
335
+ buttons: ['OK', 'Cancel']
336
+ });
337
+ return result.response === 0; // true if OK clicked
338
+ });
package/electron/main.js CHANGED
@@ -70,8 +70,13 @@ function setupSerialPortHandlers(ses) {
70
70
  // Try to find ESP-compatible port
71
71
  const espPort = portList.find(port => {
72
72
  const name = (port.displayName || port.portName || '').toLowerCase();
73
- return name.includes('cp210') ||
74
- name.includes('ch910') ||
73
+ return name.includes('cp2102') ||
74
+ name.includes('cp2103') ||
75
+ name.includes('cp2104') ||
76
+ name.includes('cp2105') ||
77
+ name.includes('cp2108') ||
78
+ name.includes('ch9102') ||
79
+ name.includes('ch9104') ||
75
80
  name.includes('ch340') ||
76
81
  name.includes('ch341') ||
77
82
  name.includes('ch343') ||
package/favicon.ico ADDED
Binary file
@@ -0,0 +1,127 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ if (!fs.existsSync('dist/cli.js')) {
5
+ console.error('Run npm run build first!');
6
+ process.exit(1);
7
+ }
8
+
9
+ function fixImportsInFile(filePath) {
10
+ let content = fs.readFileSync(filePath, 'utf8');
11
+
12
+ // Determine the relative path depth (how many ../ we need)
13
+ const depth = filePath.split(path.sep).length - 2; // -2 for 'dist' and filename
14
+ const prefix = depth > 0 ? '../'.repeat(depth) : './';
15
+
16
+ // Fix imports with single quotes: add .js extension to relative imports
17
+ content = content.replace(/from '\.\.\/([^']+)'/g, function(match, moduleName) {
18
+ if (moduleName.endsWith('.js') || moduleName.endsWith('.json')) {
19
+ return match;
20
+ }
21
+ if (moduleName === 'stubs') {
22
+ return `from '../stubs/index.js'`;
23
+ }
24
+ return `from '../${moduleName}.js'`;
25
+ });
26
+
27
+ content = content.replace(/from '\.\/([^']+)'/g, function(match, moduleName) {
28
+ if (moduleName.endsWith('.js') || moduleName.endsWith('.json')) {
29
+ return match;
30
+ }
31
+ if (moduleName === 'stubs') {
32
+ return `from './stubs/index.js'`;
33
+ }
34
+ return `from './${moduleName}.js'`;
35
+ });
36
+
37
+ // Fix imports with double quotes
38
+ content = content.replace(/from "\.\.\/([^"]+)"/g, function(match, moduleName) {
39
+ if (moduleName.endsWith('.js') || moduleName.endsWith('.json')) {
40
+ return match;
41
+ }
42
+ if (moduleName === 'stubs') {
43
+ return `from "../stubs/index.js"`;
44
+ }
45
+ return `from "../${moduleName}.js"`;
46
+ });
47
+
48
+ content = content.replace(/from "\.\/([^"]+)"/g, function(match, moduleName) {
49
+ if (moduleName.endsWith('.js') || moduleName.endsWith('.json')) {
50
+ return match;
51
+ }
52
+ if (moduleName === 'stubs') {
53
+ return `from "./stubs/index.js"`;
54
+ }
55
+ return `from "./${moduleName}.js"`;
56
+ });
57
+
58
+ // Fix dynamic JSON imports and add .default access
59
+ // Pattern: stubcode = await import("./esp32c3.json", { with: { type: "json" } });
60
+ // Replace with: stubcode = (await import("./esp32c3.json", { with: { type: "json" } })).default;
61
+ content = content.replace(
62
+ /(\w+)\s*=\s*await\s+import\("\.\/([^"]+\.json)",\s*\{\s*with:\s*\{\s*type:\s*"json"\s*\}\s*\}\);/g,
63
+ function(match, varName, jsonFile) {
64
+ return `${varName} = (await import("./${jsonFile}", { with: { type: "json" } })).default;`;
65
+ }
66
+ );
67
+
68
+ // Also handle JSON imports without .default that were already processed
69
+ // Pattern: stubcode = await import("./esp32c3.json", { with: { type: "json" } });
70
+ // This catches cases where the import is already formatted but missing .default
71
+ content = content.replace(
72
+ /(\w+)\s*=\s*await\s+import\(([^)]+\.json[^)]*)\);/g,
73
+ function(match, varName, importPath) {
74
+ // Skip if already has .default
75
+ if (match.includes('.default')) {
76
+ return match;
77
+ }
78
+ return `${varName} = (await import(${importPath})).default;`;
79
+ }
80
+ );
81
+
82
+ // Also fix dynamic imports for .js files
83
+ content = content.replace(/import\("\.\/([^"]+)"\)/g, function(match, moduleName) {
84
+ if (moduleName.endsWith('.js')) {
85
+ return match;
86
+ }
87
+ if (moduleName.endsWith('.json')) {
88
+ // Add JSON import assertion for Node.js ES modules
89
+ return `import("./${moduleName}", { with: { type: "json" } })`;
90
+ }
91
+ return `import("./${moduleName}.js")`;
92
+ });
93
+
94
+ fs.writeFileSync(filePath, content);
95
+ }
96
+
97
+ function fixAllJsFiles(dir) {
98
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
99
+
100
+ for (const entry of entries) {
101
+ const fullPath = path.join(dir, entry.name);
102
+
103
+ if (entry.isDirectory()) {
104
+ // Recursively fix subdirectories
105
+ fixAllJsFiles(fullPath);
106
+ } else if (entry.isFile() && entry.name.endsWith('.js') && entry.name !== 'cli-fixed.js') {
107
+ fixImportsInFile(fullPath);
108
+ }
109
+ }
110
+ }
111
+
112
+ console.log('Fixing imports in all .js files...');
113
+ fixAllJsFiles('dist');
114
+
115
+ // Now create cli-fixed.js with shebang
116
+ let cliSrc = fs.readFileSync('dist/cli.js', 'utf8');
117
+
118
+ // Remove shebang if present
119
+ if (cliSrc.startsWith('#!')) {
120
+ cliSrc = cliSrc.substring(cliSrc.indexOf('\n') + 1);
121
+ }
122
+
123
+ // Write fixed version with shebang
124
+ fs.writeFileSync('dist/cli-fixed.js', '#!/usr/bin/env node\n' + cliSrc);
125
+ fs.chmodSync('dist/cli-fixed.js', 0o755);
126
+
127
+ console.log('CLI built: dist/cli-fixed.js');
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+
3
+ # Icon Generator für ESP32Tool PWA
4
+ # Erstellt einfache Placeholder-Icons falls ImageMagick installiert ist
5
+
6
+ echo "🎨 ESP32Tool Icon Generator"
7
+ echo ""
8
+
9
+ # Prüfe ob ImageMagick installiert ist
10
+ if ! command -v convert &> /dev/null; then
11
+ echo "❌ ImageMagick ist nicht installiert!"
12
+ echo ""
13
+ echo "Installation:"
14
+ echo " macOS: brew install imagemagick"
15
+ echo " Linux: sudo apt install imagemagick"
16
+ echo " Windows: https://imagemagick.org/script/download.php"
17
+ echo ""
18
+ echo "Alternativ: Nutze ein Online-Tool wie https://realfavicongenerator.net/"
19
+ exit 1
20
+ fi
21
+
22
+ # Erstelle icons Ordner
23
+ mkdir -p icons
24
+ mkdir -p screenshots
25
+
26
+ echo "📦 Erstelle Icon-Größen..."
27
+
28
+ # Farben
29
+ BG_COLOR="#1a1a1a"
30
+ TEXT_COLOR="#ffffff"
31
+ ACCENT_COLOR="#4CAF50"
32
+
33
+ # Erstelle Icons mit ESP32 Text
34
+ for size in 72 96 128 144 152 192 384 512; do
35
+ # Berechne Schriftgröße basierend auf Icon-Größe
36
+ fontsize=$((size / 6))
37
+
38
+ convert -size ${size}x${size} xc:${BG_COLOR} \
39
+ -fill ${ACCENT_COLOR} -draw "circle $((size/2)),$((size/2)) $((size/2)),$((size/4))" \
40
+ -fill ${TEXT_COLOR} -pointsize ${fontsize} -font "DejaVu-Sans-Bold" \
41
+ -gravity center -annotate +0-$((size/12)) "ESP32" \
42
+ -pointsize $((fontsize/2)) -annotate +0+$((size/8)) "TOOL" \
43
+ icons/icon-${size}.png
44
+
45
+ echo " ✅ icon-${size}.png"
46
+ done
47
+
48
+ # Erstelle Favicon
49
+ convert icons/icon-192.png -resize 32x32 favicon.ico
50
+ echo " ✅ favicon.ico"
51
+
52
+ # Erstelle Apple Touch Icon
53
+ convert icons/icon-192.png -resize 180x180 apple-touch-icon.png
54
+ echo " ✅ apple-touch-icon.png"
55
+
56
+ # Erstelle Placeholder Screenshots
57
+ echo ""
58
+ echo "📸 Erstelle Placeholder Screenshots..."
59
+
60
+ # Desktop Screenshot (1280x720)
61
+ convert -size 1280x720 xc:${BG_COLOR} \
62
+ -fill ${TEXT_COLOR} -pointsize 48 -font "Helvetica-Bold" \
63
+ -gravity center -annotate +0-100 "ESP32Tool" \
64
+ -pointsize 24 -annotate +0+0 "Flash & Read ESP Devices" \
65
+ -pointsize 18 -annotate +0+50 "Desktop View" \
66
+ screenshots/desktop.png
67
+ echo " ✅ desktop.png (1280x720)"
68
+
69
+ # Mobile Screenshot (540x720)
70
+ convert -size 540x720 xc:${BG_COLOR} \
71
+ -fill ${TEXT_COLOR} -pointsize 36 -font "Helvetica-Bold" \
72
+ -gravity center -annotate +0-100 "ESP32Tool" \
73
+ -pointsize 18 -annotate +0+0 "Flash & Read" \
74
+ -pointsize 18 -annotate +0+30 "ESP Devices" \
75
+ -pointsize 14 -annotate +0+80 "Mobile View" \
76
+ screenshots/mobile.png
77
+ echo " ✅ mobile.png (540x720)"
78
+
79
+ echo ""
80
+ echo "✨ Fertig! Icons und Screenshots wurden erstellt."
81
+ echo ""
82
+ echo "📁 Dateien:"
83
+ echo " - icons/icon-*.png (8 Größen)"
84
+ echo " - favicon.ico"
85
+ echo " - apple-touch-icon.png"
86
+ echo " - screenshots/*.png"
87
+ echo ""
88
+ echo "💡 Tipp: Ersetze die Placeholder-Icons mit deinem eigenen Logo!"
89
+ echo " Nutze dazu: convert dein-logo.png -resize 512x512 icons/icon-512.png"
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file