@jjlmoya/utils-drones 1.20.0 → 1.22.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 (36) hide show
  1. package/package.json +2 -2
  2. package/src/category/index.ts +14 -6
  3. package/src/category/seo.astro +1 -1
  4. package/src/entries.ts +13 -1
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/drone-battery-c-rating-calculator/bibliography.astro +17 -0
  8. package/src/tool/drone-battery-c-rating-calculator/bibliography.ts +16 -0
  9. package/src/tool/drone-battery-c-rating-calculator/component.astro +192 -0
  10. package/src/tool/drone-battery-c-rating-calculator/controller.ts +225 -0
  11. package/src/tool/drone-battery-c-rating-calculator/dom-views.ts +119 -0
  12. package/src/tool/drone-battery-c-rating-calculator/drone-battery-c-rating-calculator.css +300 -0
  13. package/src/tool/drone-battery-c-rating-calculator/entry.ts +27 -0
  14. package/src/tool/drone-battery-c-rating-calculator/evaluator.ts +37 -0
  15. package/src/tool/drone-battery-c-rating-calculator/i18n/de.ts +192 -0
  16. package/src/tool/drone-battery-c-rating-calculator/i18n/en.ts +192 -0
  17. package/src/tool/drone-battery-c-rating-calculator/i18n/es.ts +192 -0
  18. package/src/tool/drone-battery-c-rating-calculator/i18n/fr.ts +192 -0
  19. package/src/tool/drone-battery-c-rating-calculator/i18n/id.ts +192 -0
  20. package/src/tool/drone-battery-c-rating-calculator/i18n/it.ts +192 -0
  21. package/src/tool/drone-battery-c-rating-calculator/i18n/ja.ts +192 -0
  22. package/src/tool/drone-battery-c-rating-calculator/i18n/ko.ts +192 -0
  23. package/src/tool/drone-battery-c-rating-calculator/i18n/nl.ts +192 -0
  24. package/src/tool/drone-battery-c-rating-calculator/i18n/pl.ts +192 -0
  25. package/src/tool/drone-battery-c-rating-calculator/i18n/pt.ts +192 -0
  26. package/src/tool/drone-battery-c-rating-calculator/i18n/ru.ts +192 -0
  27. package/src/tool/drone-battery-c-rating-calculator/i18n/sv.ts +192 -0
  28. package/src/tool/drone-battery-c-rating-calculator/i18n/tr.ts +192 -0
  29. package/src/tool/drone-battery-c-rating-calculator/i18n/zh.ts +192 -0
  30. package/src/tool/drone-battery-c-rating-calculator/index.ts +10 -0
  31. package/src/tool/drone-battery-c-rating-calculator/logic.test.ts +84 -0
  32. package/src/tool/drone-battery-c-rating-calculator/logic.ts +112 -0
  33. package/src/tool/drone-battery-c-rating-calculator/seo.astro +16 -0
  34. package/src/tool/drone-battery-c-rating-calculator/storage.ts +19 -0
  35. package/src/tool/drone-battery-c-rating-calculator/ui.ts +43 -0
  36. package/src/tools.ts +3 -1
@@ -0,0 +1,119 @@
1
+ import type { BatteryCalculationResults, BatteryInputs, SafetyStatus } from './logic';
2
+ import type { SafetyEvaluation } from './evaluator';
3
+
4
+ export interface DOMResultElements {
5
+ claimedMaxCurrent: HTMLElement;
6
+ realisticCRating: HTMLElement;
7
+ realisticMaxCurrent: HTMLElement;
8
+ totalPeakDraw: HTMLElement;
9
+ voltageSag: HTMLElement;
10
+ sagNominalVoltage: HTMLElement;
11
+ flightTimeFullThrottle: HTMLElement;
12
+ flightTimeHover: HTMLElement;
13
+ burstRatingRequired: HTMLElement;
14
+ safetyBadge: HTMLElement;
15
+ statusTitle: HTMLElement;
16
+ statusDesc: HTMLElement;
17
+ svgContainer: HTMLElement;
18
+ }
19
+
20
+ export function formatTimeSeconds(seconds: number): string {
21
+ if (!seconds || seconds <= 0 || !Number.isFinite(seconds)) return '0s';
22
+ const mins = Math.floor(seconds / 60);
23
+ const secs = Math.round(seconds % 60);
24
+ if (mins === 0) return `${secs}s`;
25
+ return `${mins}m ${secs}s`;
26
+ }
27
+
28
+ function getSafetyColor(status: SafetyStatus): string {
29
+ if (status === 'danger') return '#ef4444';
30
+ if (status === 'warning') return '#f59e0b';
31
+ return '#10b981';
32
+ }
33
+
34
+ function renderCellsSVG(cellCount: number, statusColor: string, sagNominalVoltageV: number): string {
35
+ const cellWidth = Math.min(42, Math.floor(260 / cellCount));
36
+ return Array.from({ length: cellCount }, (_, i) => {
37
+ const x = 35 + i * (cellWidth + 5);
38
+ const cellSagV = (sagNominalVoltageV / cellCount).toFixed(2);
39
+ return `<g>
40
+ <rect x="${x}" y="55" width="${cellWidth}" height="90" rx="6" fill="var(--n-svg-inner, #1e293b)" stroke="${statusColor}" stroke-width="2"/>
41
+ <rect x="${x + 3}" y="61" width="${cellWidth - 6}" height="35" rx="3" fill="${statusColor}" fill-opacity="0.2"/>
42
+ <circle cx="${x + cellWidth / 2}" cy="78" r="4" fill="${statusColor}"/>
43
+ <text x="${x + cellWidth / 2}" y="112" fill="var(--n-text, #f8fafc)" font-size="11" font-weight="800" text-anchor="middle">S${i + 1}</text>
44
+ <text x="${x + cellWidth / 2}" y="132" fill="var(--n-text-muted, #94a3b8)" font-size="10" font-weight="600" text-anchor="middle">${cellSagV}V</text>
45
+ </g>`;
46
+ }).join('');
47
+ }
48
+
49
+ function renderGaugeHUD(results: BatteryCalculationResults, statusColor: string): string {
50
+ const maxCapA = Math.max(1, results.realisticMaxCurrentA);
51
+ const ratio = Math.min(100, Math.round((results.totalPeakDrawA / maxCapA) * 100));
52
+ const strokeDash = 251;
53
+ const strokeOffset = strokeDash - (strokeDash * ratio) / 100;
54
+ return `<g transform="translate(390, 20)">
55
+ <rect x="0" y="0" width="150" height="160" rx="14" fill="var(--n-svg-card, #0f172a)" stroke="var(--n-svg-stroke, #334155)" stroke-width="2" />
56
+ <text x="75" y="24" fill="var(--n-text-muted, #94a3b8)" font-size="10" font-weight="700" text-anchor="middle" letter-spacing="0.5">CURRENT STRESS</text>
57
+ <circle cx="75" cy="85" r="40" fill="none" stroke="var(--n-svg-inner, #1e293b)" stroke-width="9" />
58
+ <circle cx="75" cy="85" r="40" fill="none" stroke="${statusColor}" stroke-width="9" stroke-dasharray="${strokeDash}" stroke-dashoffset="${strokeOffset}" stroke-linecap="round" transform="rotate(-90 75 85)" />
59
+ <text x="75" y="88" fill="var(--n-text, #f8fafc)" font-size="16" font-weight="900" text-anchor="middle">${ratio}%</text>
60
+ <text x="75" y="102" fill="var(--n-text-muted, #94a3b8)" font-size="9" text-anchor="middle">PEAK LOAD</text>
61
+ <rect x="15" y="125" width="120" height="24" rx="12" fill="${statusColor}" fill-opacity="0.2" stroke="${statusColor}" stroke-width="1"/>
62
+ <text x="75" y="141" fill="${statusColor}" font-size="10" font-weight="800" text-anchor="middle" letter-spacing="0.5">${results.safetyStatus.toUpperCase()}</text>
63
+ </g>`;
64
+ }
65
+
66
+ export function renderBatterySVG(inputs: BatteryInputs, results: BatteryCalculationResults): string {
67
+ const statusColor = getSafetyColor(results.safetyStatus);
68
+ const cellCount = Math.max(1, Math.min(8, inputs.cellCount));
69
+ const cellsHtml = renderCellsSVG(cellCount, statusColor, results.sagNominalVoltageV);
70
+ const gaugeHtml = renderGaugeHUD(results, statusColor);
71
+
72
+ return `<svg viewBox="0 0 560 210" width="100%" height="210" xmlns="http://www.w3.org/2000/svg">
73
+ <defs>
74
+ <filter id="glowEffect" x="-20%" y="-20%" width="140%" height="140%">
75
+ <feGaussianBlur stdDeviation="5" result="blur" />
76
+ <feComposite in="SourceGraphic" in2="blur" operator="over" />
77
+ </filter>
78
+ </defs>
79
+ <path d="M 310 100 Q 340 70 370 100" stroke="#ef4444" stroke-width="5" fill="none" stroke-linecap="round"/>
80
+ <path d="M 310 110 Q 340 140 370 110" stroke="var(--n-text, #000000)" stroke-width="5" fill="none" stroke-linecap="round"/>
81
+ <rect x="365" y="95" width="22" height="20" rx="4" fill="#eab308" stroke="#ca8a04" stroke-width="2"/>
82
+ <rect x="20" y="30" width="310" height="140" rx="14" fill="var(--n-svg-card, #0f172a)" stroke="var(--n-svg-stroke, #334155)" stroke-width="2"/>
83
+ <rect x="20" y="30" width="310" height="140" rx="14" fill="none" stroke="${statusColor}" stroke-width="2" filter="url(#glowEffect)" opacity="0.4"/>
84
+ <text x="35" y="48" fill="var(--n-primary, #0284c7)" font-size="12" font-weight="800" letter-spacing="1">${inputs.cellCount}S ${inputs.chemistry.toUpperCase()} PACK (${inputs.capacitymAh} mAh)</text>
85
+ <g>${cellsHtml}</g>
86
+ ${gaugeHtml}
87
+ <text x="35" y="192" fill="${statusColor}" font-size="11" font-weight="700">Real Discharge: ${results.realisticMaxCurrentA.toFixed(1)}A (${results.realisticCRating}C)</text>
88
+ <text x="330" y="192" fill="var(--n-text-muted, #94a3b8)" font-size="11" font-weight="600" text-anchor="end">Peak Draw: ${results.totalPeakDrawA.toFixed(1)}A</text>
89
+ </svg>`;
90
+ }
91
+
92
+ function updatePrimaryMetrics(elements: DOMResultElements, results: BatteryCalculationResults): void {
93
+ if (elements.claimedMaxCurrent) elements.claimedMaxCurrent.textContent = `${results.claimedMaxCurrentA.toFixed(1)} A`;
94
+ if (elements.realisticCRating) elements.realisticCRating.textContent = `${results.realisticCRating} C`;
95
+ if (elements.realisticMaxCurrent) elements.realisticMaxCurrent.textContent = `${results.realisticMaxCurrentA.toFixed(1)} A`;
96
+ if (elements.totalPeakDraw) elements.totalPeakDraw.textContent = `${results.totalPeakDrawA.toFixed(1)} A`;
97
+ if (elements.voltageSag) elements.voltageSag.textContent = `-${results.voltageSagV.toFixed(2)} V`;
98
+ }
99
+
100
+ function updateSecondaryMetrics(elements: DOMResultElements, results: BatteryCalculationResults): void {
101
+ if (elements.sagNominalVoltage) elements.sagNominalVoltage.textContent = `${results.sagNominalVoltageV.toFixed(1)} V`;
102
+ if (elements.flightTimeFullThrottle) elements.flightTimeFullThrottle.textContent = formatTimeSeconds(results.flightTimeFullThrottleSec);
103
+ if (elements.flightTimeHover) elements.flightTimeHover.textContent = formatTimeSeconds(results.flightTimeHoverSec);
104
+ if (elements.burstRatingRequired) elements.burstRatingRequired.textContent = `${results.burstRatingRequired.toFixed(0)} C`;
105
+ }
106
+
107
+ export function updateDOMMetrics(elements: DOMResultElements, results: BatteryCalculationResults): void {
108
+ updatePrimaryMetrics(elements, results);
109
+ updateSecondaryMetrics(elements, results);
110
+ }
111
+
112
+ export function updateDOMSafety(elements: DOMResultElements, evalData: SafetyEvaluation): void {
113
+ if (elements.safetyBadge) {
114
+ elements.safetyBadge.className = `sc-safety-badge ${evalData.badgeClass}`;
115
+ elements.safetyBadge.textContent = evalData.title;
116
+ }
117
+ if (elements.statusTitle) elements.statusTitle.textContent = evalData.title;
118
+ if (elements.statusDesc) elements.statusDesc.textContent = evalData.description;
119
+ }
@@ -0,0 +1,300 @@
1
+ :root {
2
+ --n-bg: #f8fafc;
3
+ --n-surface: #fff;
4
+ --n-card-bg: rgba(255, 255, 255, 0.95);
5
+ --n-border: #cbd5e1;
6
+ --n-text: #0f172a;
7
+ --n-text-muted: #475569;
8
+ --n-primary: #0284c7;
9
+ --n-primary-hover: #0369a1;
10
+ --n-accent: #38bdf8;
11
+ --n-success: #10b981;
12
+ --n-warning: #f59e0b;
13
+ --n-danger: #ef4444;
14
+ --n-svg-card: #f1f5f9;
15
+ --n-svg-inner: #fff;
16
+ --n-svg-stroke: #cbd5e1;
17
+ --n-radius: 16px;
18
+ --n-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.08);
19
+ }
20
+
21
+ .theme-dark {
22
+ --n-bg: #0b0f19;
23
+ --n-surface: #111827;
24
+ --n-card-bg: rgba(17, 24, 39, 0.9);
25
+ --n-border: #1f2937;
26
+ --n-text: #f9fafb;
27
+ --n-text-muted: #9ca3af;
28
+ --n-primary: #38bdf8;
29
+ --n-primary-hover: #7dd3fc;
30
+ --n-accent: #0ea5e9;
31
+ --n-success: #34d399;
32
+ --n-warning: #fbbf24;
33
+ --n-danger: #f87171;
34
+ --n-svg-card: #0f172a;
35
+ --n-svg-inner: #1e293b;
36
+ --n-svg-stroke: #334155;
37
+ --n-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.5);
38
+ }
39
+
40
+ .sc-main-card {
41
+ display: grid;
42
+ grid-template-columns: 1fr;
43
+ gap: 2rem;
44
+ background-color: var(--n-card-bg);
45
+ border: 1px solid var(--n-border);
46
+ border-radius: var(--n-radius);
47
+ padding: 2rem;
48
+ box-shadow: var(--n-shadow);
49
+ backdrop-filter: blur(12px);
50
+ color: var(--n-text);
51
+ margin-bottom: 2rem;
52
+ }
53
+
54
+ @media (min-width: 1024px) {
55
+ .sc-main-card {
56
+ grid-template-columns: 380px 1fr;
57
+ }
58
+ }
59
+
60
+ .sc-sidebar {
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 1.5rem;
64
+ }
65
+
66
+ .sc-section-title {
67
+ font-size: 1.1rem;
68
+ font-weight: 700;
69
+ color: var(--n-text);
70
+ margin-bottom: 0.8rem;
71
+ }
72
+
73
+ .sc-preset-group {
74
+ display: flex;
75
+ flex-wrap: wrap;
76
+ gap: 0.5rem;
77
+ }
78
+
79
+ .sc-preset-chip {
80
+ background-color: var(--n-surface);
81
+ border: 1px solid var(--n-border);
82
+ color: var(--n-text);
83
+ border-radius: 8px;
84
+ padding: 0.45rem 0.85rem;
85
+ font-size: 0.85rem;
86
+ font-weight: 600;
87
+ cursor: pointer;
88
+ transition: all 0.2s ease;
89
+ }
90
+
91
+ .sc-preset-chip:hover {
92
+ border-color: var(--n-primary);
93
+ color: var(--n-primary);
94
+ }
95
+
96
+ .sc-preset-chip.active {
97
+ background-color: var(--n-primary);
98
+ border-color: var(--n-primary);
99
+ color: #fff;
100
+ box-shadow: 0 0 12px rgba(56, 189, 248, 0.3);
101
+ }
102
+
103
+ .sc-field-group {
104
+ display: flex;
105
+ flex-direction: column;
106
+ gap: 0.4rem;
107
+ }
108
+
109
+ .sc-field-header {
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: space-between;
113
+ }
114
+
115
+ .sc-field-header label {
116
+ font-size: 0.85rem;
117
+ font-weight: 600;
118
+ color: var(--n-text-muted);
119
+ }
120
+
121
+ .sc-input {
122
+ width: 100%;
123
+ background-color: var(--n-surface);
124
+ border: 1px solid var(--n-border);
125
+ border-radius: 8px;
126
+ padding: 0.55rem 0.8rem;
127
+ color: var(--n-text);
128
+ font-size: 0.95rem;
129
+ outline: none;
130
+ transition: border-color 0.2s ease;
131
+ }
132
+
133
+ .sc-input:focus {
134
+ border-color: var(--n-primary);
135
+ }
136
+
137
+ .sc-slider {
138
+ width: 100%;
139
+ accent-color: var(--n-primary);
140
+ cursor: pointer;
141
+ margin-top: 0.2rem;
142
+ }
143
+
144
+ .sc-custom-select {
145
+ position: relative;
146
+ width: 100%;
147
+ }
148
+
149
+ .sc-select-trigger {
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: space-between;
153
+ background-color: var(--n-surface);
154
+ border: 1px solid var(--n-border);
155
+ border-radius: 8px;
156
+ padding: 0.6rem 0.8rem;
157
+ color: var(--n-text);
158
+ font-size: 0.95rem;
159
+ cursor: pointer;
160
+ user-select: none;
161
+ }
162
+
163
+ .sc-select-trigger svg {
164
+ transition: transform 0.2s ease;
165
+ fill: var(--n-text-muted);
166
+ }
167
+
168
+ .sc-custom-select.open .sc-select-trigger svg {
169
+ transform: rotate(180deg);
170
+ }
171
+
172
+ .sc-select-dropdown {
173
+ display: none;
174
+ position: absolute;
175
+ top: calc(100% + 4px);
176
+ left: 0;
177
+ right: 0;
178
+ background-color: var(--n-surface);
179
+ border: 1px solid var(--n-border);
180
+ border-radius: 8px;
181
+ box-shadow: var(--n-shadow);
182
+ z-index: 50;
183
+ overflow: hidden;
184
+ }
185
+
186
+ .sc-custom-select.open .sc-select-dropdown {
187
+ display: block;
188
+ }
189
+
190
+ .sc-select-option {
191
+ padding: 0.6rem 0.8rem;
192
+ font-size: 0.9rem;
193
+ color: var(--n-text);
194
+ cursor: pointer;
195
+ transition: background-color 0.2s ease;
196
+ }
197
+
198
+ .sc-select-option:hover,
199
+ .sc-select-option.active {
200
+ background-color: var(--n-primary);
201
+ color: #fff;
202
+ }
203
+
204
+ .sc-display-panel {
205
+ display: flex;
206
+ flex-direction: column;
207
+ gap: 1.5rem;
208
+ }
209
+
210
+ .sc-header-bar {
211
+ display: flex;
212
+ align-items: center;
213
+ justify-content: space-between;
214
+ gap: 1rem;
215
+ }
216
+
217
+ .sc-safety-badge {
218
+ padding: 0.5rem 1.2rem;
219
+ border-radius: 20px;
220
+ font-weight: 700;
221
+ font-size: 0.85rem;
222
+ text-transform: uppercase;
223
+ letter-spacing: 0.05em;
224
+ }
225
+
226
+ .status-optimal {
227
+ background-color: rgba(16, 185, 129, 0.15);
228
+ color: var(--n-success);
229
+ border: 1px solid var(--n-success);
230
+ }
231
+
232
+ .status-warning {
233
+ background-color: rgba(245, 158, 11, 0.15);
234
+ color: var(--n-warning);
235
+ border: 1px solid var(--n-warning);
236
+ }
237
+
238
+ .status-danger {
239
+ background-color: rgba(239, 68, 68, 0.15);
240
+ color: var(--n-danger);
241
+ border: 1px solid var(--n-danger);
242
+ }
243
+
244
+ .sc-svg-wrapper {
245
+ background-color: var(--n-surface);
246
+ border: 1px solid var(--n-border);
247
+ border-radius: 12px;
248
+ padding: 1rem;
249
+ display: flex;
250
+ justify-content: center;
251
+ align-items: center;
252
+ }
253
+
254
+ .sc-results-grid {
255
+ display: grid;
256
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
257
+ gap: 1rem;
258
+ }
259
+
260
+ .sc-metric-card {
261
+ background-color: var(--n-surface);
262
+ border: 1px solid var(--n-border);
263
+ border-radius: 12px;
264
+ padding: 1rem;
265
+ display: flex;
266
+ flex-direction: column;
267
+ gap: 0.4rem;
268
+ }
269
+
270
+ .sc-metric-label {
271
+ font-size: 0.8rem;
272
+ color: var(--n-text-muted);
273
+ font-weight: 600;
274
+ }
275
+
276
+ .sc-metric-value {
277
+ font-size: 1.35rem;
278
+ font-weight: 800;
279
+ color: var(--n-text);
280
+ }
281
+
282
+ .sc-status-box {
283
+ background-color: var(--n-surface);
284
+ border: 1px solid var(--n-border);
285
+ border-radius: 12px;
286
+ padding: 1.2rem;
287
+ }
288
+
289
+ .sc-status-box h4 {
290
+ font-size: 1rem;
291
+ font-weight: 700;
292
+ margin-bottom: 0.4rem;
293
+ color: var(--n-text);
294
+ }
295
+
296
+ .sc-status-box p {
297
+ font-size: 0.9rem;
298
+ color: var(--n-text-muted);
299
+ line-height: 1.5;
300
+ }
@@ -0,0 +1,27 @@
1
+ import type { DronesToolEntry, ToolLocaleContent } from '../../types';
2
+ import type { DroneBatteryCRatingCalculatorUI } from './ui';
3
+
4
+ export type { DroneBatteryCRatingCalculatorUI };
5
+ export type DroneBatteryCRatingCalculatorLocaleContent = ToolLocaleContent<DroneBatteryCRatingCalculatorUI>;
6
+
7
+ export const droneBatteryCRatingCalculator: DronesToolEntry<DroneBatteryCRatingCalculatorUI> = {
8
+ id: 'drone-battery-c-rating-calculator',
9
+ icons: { bg: 'mdi:battery-charging-100', fg: 'mdi:flash-auto' },
10
+ i18n: {
11
+ es: () => import('./i18n/es').then((m) => m.content),
12
+ en: () => import('./i18n/en').then((m) => m.content),
13
+ fr: () => import('./i18n/fr').then((m) => m.content),
14
+ de: () => import('./i18n/de').then((m) => m.content),
15
+ it: () => import('./i18n/it').then((m) => m.content),
16
+ pt: () => import('./i18n/pt').then((m) => m.content),
17
+ nl: () => import('./i18n/nl').then((m) => m.content),
18
+ pl: () => import('./i18n/pl').then((m) => m.content),
19
+ ru: () => import('./i18n/ru').then((m) => m.content),
20
+ ja: () => import('./i18n/ja').then((m) => m.content),
21
+ ko: () => import('./i18n/ko').then((m) => m.content),
22
+ zh: () => import('./i18n/zh').then((m) => m.content),
23
+ tr: () => import('./i18n/tr').then((m) => m.content),
24
+ sv: () => import('./i18n/sv').then((m) => m.content),
25
+ id: () => import('./i18n/id').then((m) => m.content),
26
+ },
27
+ };
@@ -0,0 +1,37 @@
1
+ import type { SafetyStatus } from './logic';
2
+ import type { DroneBatteryCRatingCalculatorUI } from './ui';
3
+
4
+ export interface SafetyEvaluation {
5
+ status: SafetyStatus;
6
+ badgeClass: string;
7
+ title: string;
8
+ description: string;
9
+ }
10
+
11
+ export function evaluateSafetyPresentation(
12
+ status: SafetyStatus,
13
+ ui: DroneBatteryCRatingCalculatorUI
14
+ ): SafetyEvaluation {
15
+ if (status === 'optimal') {
16
+ return {
17
+ status,
18
+ badgeClass: 'status-optimal',
19
+ title: ui.statusOptimalTitle,
20
+ description: ui.statusOptimalDesc,
21
+ };
22
+ }
23
+ if (status === 'warning') {
24
+ return {
25
+ status,
26
+ badgeClass: 'status-warning',
27
+ title: ui.statusWarningTitle,
28
+ description: ui.statusWarningDesc,
29
+ };
30
+ }
31
+ return {
32
+ status,
33
+ badgeClass: 'status-danger',
34
+ title: ui.statusDangerTitle,
35
+ description: ui.statusDangerDesc,
36
+ };
37
+ }
@@ -0,0 +1,192 @@
1
+ import type { DroneBatteryCRatingCalculatorLocaleContent } from '../entry';
2
+ import type { WithContext, SoftwareApplication, FAQPage, HowTo } from 'schema-dts';
3
+ import type { SEOSection } from '../../../types';
4
+ import { BIBLIOGRAPHY_ITEMS } from '../bibliography';
5
+
6
+ const slug = 'lipo-c-rate-rechner-drohne';
7
+ const title = 'Drohnen LiPo Akku C Rate und Dauerentladungs Rechner';
8
+ const description = 'Berechnen Sie den realistischen Dauerentladestrom, die C-Rate, den Spannungsabfall und die Flugsicherheit von LiPo-Akkus für Drohnen basierend auf Innenwiderstand und Motorverbrauch.';
9
+
10
+ const ui = {
11
+ title: 'Drohnen LiPo Akku C Rate Rechner',
12
+ subtitle: 'Analysieren Sie reale Dauerentladung Peak Anforderungen und Spannungsabfall für Multikopter',
13
+ lipoSpecsHeader: 'Akkuspezifikationen',
14
+ capacityLabel: 'Kapazität (mAh)',
15
+ claimedCRatingLabel: 'Angegebene C Rate',
16
+ cellCountLabel: 'Zellenzahl (Serie)',
17
+ chemistryLabel: 'Akkuchemie',
18
+ internalResistanceLabel: 'Innenwiderstand pro Zelle (mΩ)',
19
+ quadSpecsHeader: 'Stromverbrauch des Kopters',
20
+ motorCountLabel: 'Motoranzahl',
21
+ peakMotorCurrentLabel: 'Spitzenstrom pro Motor (Ampere)',
22
+ auxCurrentLabel: 'Zusatzverbraucher (VTX FC Kamera) (Ampere)',
23
+ presetSelectLabel: 'Schnelleinstellungen',
24
+ customPreset: 'Benutzerdefiniert',
25
+ whoopPreset: '1S TinyWhoop',
26
+ freestyle5Preset: '6S 5 Zoll Freestyle',
27
+ cinewhoopPreset: '4S 3 Zoll CineWhoop',
28
+ longRange7Preset: '6S 7 Zoll Long Range',
29
+ racing5Preset: '6S 5 Zoll Racing',
30
+ resultsHeader: 'Leistungs und Performance Analyse',
31
+ claimedMaxCurrentLabel: 'Angegebener Maximalstrom',
32
+ realisticCRatingLabel: 'Realistische Dauer C Rate',
33
+ realisticMaxCurrentLabel: 'Realistischer Dauerstrom',
34
+ totalPeakDrawLabel: 'Gesamter Spitzenstrom',
35
+ voltageSagLabel: 'Geschätzter Spannungsabfall',
36
+ sagNominalVoltageLabel: 'Nennspannung unter Last',
37
+ flightTimeFullThrottleLabel: 'Vollgas Flugzeit',
38
+ flightTimeHoverLabel: 'Geschätzte Schwebeflugzeit',
39
+ safetyStatusLabel: 'Sicherheitsdiagnose',
40
+ statusOptimalTitle: 'Sicherer und Optimaler Akku',
41
+ statusOptimalDesc: 'Der Akku kann den Spitzenstrom ohne übermäßige Erwärmung oder starken Spannungsabfall problemlos liefern. Hohe Lebensdauer der Zellen garantiert.',
42
+ statusWarningTitle: 'Moderate Thermische und Spannungs Belastung',
43
+ statusWarningDesc: 'Der Spitzenstrom liegt nahe an der realistischen Akkugrenze. Bei schnellen Vollgas Punches ist mit leichtem Voltage Sag zu rechnen.',
44
+ statusDangerTitle: 'Hohes Überlastungs und Sag Risiko',
45
+ statusDangerDesc: 'Der Spitzenstrom übersteigt die reale Kapazität des Akkus. Hohes Risiko von Voltage Sag, Zellüberhitzung und vorzeitiger Alterung.',
46
+ lipoVisualizerTitle: 'Live LiPo Status Visualisierung',
47
+ cellVoltageLabel: 'Zellenspannung',
48
+ batteryHealthLabel: 'Akkubelastung',
49
+ burstRatingRequiredLabel: 'Erforderliche Peak C-Rate',
50
+ currentRatioLabel: 'Strom-Lastverhältnis',
51
+ };
52
+
53
+ const faqItems = [
54
+ {
55
+ question: 'Was bedeutet die C-Rate bei LiPo-Akkus?',
56
+ answer: 'Die C-Rate gibt die maximale kontinuierliche Entladerate im Verhältnis zur Akkukapazität an. Ein 1500-mAh-Akku mit 100C kann theoretisch 150 Ampere liefern.',
57
+ },
58
+ {
59
+ question: 'Warum unterscheidet sich die beworbene von der realen C-Rate?',
60
+ answer: 'Hersteller werben oft mit Peak-Werten unter Laborbedingungen. Die tatsächliche Dauerleistung hängt direkt vom Innenwiderstand der einzelnen Zellen ab.',
61
+ },
62
+ {
63
+ question: 'Wie wirkt sich der Innenwiderstand auf Spannung und Hitze aus?',
64
+ answer: 'Ein hoher Innenwiderstand wirkt wie ein störender Widerstand in der Zelle. Bei hoher Stromabgabe fällt die Spannung ab und Energie wird in Hitze umgewandelt.',
65
+ },
66
+ {
67
+ question: 'Wie vermeide ich Voltage Sag beim Freestyle-Fliegen?',
68
+ answer: 'Verwenden Sie Akkus mit niedrigem Innenwiderstand, wählen Sie eine Sicherheitsreserve von mindestens 15 Prozent über dem Spitzenverbrauch und fliegen Sie nicht unter 3.5V pro Zelle.',
69
+ },
70
+ ];
71
+
72
+ const howToSteps = [
73
+ {
74
+ name: 'Voreinstellung wählen oder Akkudaten eingeben',
75
+ text: 'Geben Sie Kapazität in mAh, angegebene C-Rate, Zellenzahl und den durchschnittlichen Innenwiderstand pro Zelle ein.',
76
+ },
77
+ {
78
+ name: 'Motoren und Elektronik konfigurieren',
79
+ text: 'Tragen Sie die Anzahl der Motoren, den Spitzenstrom pro Motor bei Vollgas sowie den Zusatzverbrauch ein.',
80
+ },
81
+ {
82
+ name: 'Sicherheitsdiagnose und Realamperestrom prüfen',
83
+ text: 'Vergleichen Sie den realistischen Dauerstrom mit dem Spitzenverbrauch des Kopters für einen sicheren Flug.',
84
+ },
85
+ ];
86
+
87
+ const seoSections: SEOSection[] = [
88
+ {
89
+ type: 'title',
90
+ text: 'Verständnis von LiPo C-Rate und Realleistung bei Drohnen',
91
+ level: 2,
92
+ },
93
+ {
94
+ type: 'paragraph',
95
+ html: 'Die Auswahl des richtigen LiPo-Akkus für eine FPV-Drohne erfordert das Verständnis der Zusammenhänge zwischen Kapazität, C-Rate und Stromverbrauch. Während Hersteller oft Werte von 100C oder mehr angeben, wird die reale Dauerentladung durch den Innenwiderstand und die Wärmeableitung begrenzt. Dieser Rechner ermittelt die realistische Dauerstromabgabe mit echten Sicherheitsreserven.',
96
+ },
97
+ {
98
+ type: 'title',
99
+ text: 'Vergleichstabelle für RC Akku Chemien',
100
+ level: 2,
101
+ },
102
+ {
103
+ type: 'table',
104
+ headers: ['Chemie', 'Nennspannung', 'Max. Spannung', 'Energiedichte', 'Peak Entladung', 'Empfohlener Einsatz'],
105
+ rows: [
106
+ ['LiPo (Standard)', '3.7V', '4.20V', 'Hoch', '100C - 150C', '5 Zoll FPV Freestyle und Racing'],
107
+ ['LiHV (High Voltage)', '3.8V', '4.35V', 'Sehr hoch', '80C - 120C', 'TinyWhoops und Mikro-Kopter'],
108
+ ['Li-Ion (18650/21700)', '3.6V', '4.20V', 'Maximum', '15C - 35C', '7 Zoll Long Range Kopter'],
109
+ ['LiFePO4', '3.3V', '3.65V', 'Moderat', '30C - 50C', 'Ladestationen auf dem Feld'],
110
+ ],
111
+ },
112
+ {
113
+ type: 'title',
114
+ text: 'Einfluss von Voltage Sag und Innenwiderstand auf die Performance',
115
+ level: 2,
116
+ },
117
+ {
118
+ type: 'paragraph',
119
+ html: 'Voltage Sag bezeichnet den plötzlichen Spannungseinbruch unter hoher Last. Wenn Strom durch den Innenwiderstand fließt, entsteht Wärme statt Schub. Ein gealterter Akku führt zu frühzeitigen Niedrigspannungswarnungen im OSD der FPV-Brille.',
120
+ },
121
+ {
122
+ type: 'list',
123
+ items: [
124
+ 'Niedriger Innenwiderstand (1-4 mΩ pro Zelle): Maximale Leistung, minimaler Sag und kühle Temperaturen.',
125
+ 'Moderater Innenwiderstand (5-10 mΩ pro Zelle): Solide Standard-Performance für Freestyle.',
126
+ 'Hoher Innenwiderstand (>12 mΩ pro Zelle): Deutlicher Leistungsverlust, starker Sag und rasche Erwärmung.',
127
+ ],
128
+ },
129
+ {
130
+ type: 'title',
131
+ text: 'Akkuoptimierung für Freestyle Racing und Long Range',
132
+ level: 2,
133
+ },
134
+ {
135
+ type: 'paragraph',
136
+ html: 'Jeder Flugstil stellt unterschiedliche Anforderungen an die Energieversorgung. 5-Zoll-Freestyle-Kopter erzeugen Stromspitzen von über 120 Ampere, während 7-Zoll-Long-Range-Kopter gleichmäßige Effizienz benötigen. Die passende Abstimmung schützt vor Stromausfällen im Flug.',
137
+ },
138
+ {
139
+ type: 'tip',
140
+ title: 'Tipp zur Akkupflege',
141
+ html: 'Lagern Sie Ihre LiPo-Akkus bei Nichtgebrauch stets bei 3.80V bis 3.85V pro Zelle. Voll geladene Akkus, die länger als 48 Stunden liegen gelassen werden, verringern dauerhaft ihre Leistungsfähigkeit.',
142
+ },
143
+ ];
144
+
145
+ const schemas: DroneBatteryCRatingCalculatorLocaleContent['schemas'] = [
146
+ {
147
+ '@context': 'https://schema.org',
148
+ '@type': 'FAQPage',
149
+ mainEntity: faqItems.map((item) => ({
150
+ '@type': 'Question',
151
+ name: item.question,
152
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
153
+ })),
154
+ } as WithContext<FAQPage>,
155
+ {
156
+ '@context': 'https://schema.org',
157
+ '@type': 'HowTo',
158
+ name: title,
159
+ description,
160
+ step: howToSteps.map((s, index) => ({
161
+ '@type': 'HowToStep',
162
+ position: index + 1,
163
+ name: s.name,
164
+ text: s.text,
165
+ })),
166
+ } as WithContext<HowTo>,
167
+ {
168
+ '@context': 'https://schema.org',
169
+ '@type': 'SoftwareApplication',
170
+ name: title,
171
+ description,
172
+ applicationCategory: 'UtilityApplication',
173
+ operatingSystem: 'All',
174
+ offers: {
175
+ '@type': 'Offer',
176
+ price: '0',
177
+ priceCurrency: 'EUR',
178
+ },
179
+ } as WithContext<SoftwareApplication>,
180
+ ];
181
+
182
+ export const content: DroneBatteryCRatingCalculatorLocaleContent = {
183
+ slug,
184
+ title,
185
+ description,
186
+ ui,
187
+ seo: seoSections,
188
+ faq: faqItems,
189
+ howTo: howToSteps,
190
+ bibliography: BIBLIOGRAPHY_ITEMS,
191
+ schemas,
192
+ };