Added support for DST
New Web UI
Various fixes in main .ino
This commit is contained in:
mfactory-osaka
2025-06-10 17:33:03 +09:00
parent 772286dbd9
commit 10ac138a85
7 changed files with 981 additions and 485 deletions

File diff suppressed because it is too large Load Diff

BIN
assets/webui2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

View File

@@ -2,10 +2,10 @@
"ssid": "",
"password": "",
"openWeatherApiKey": "ADD-YOUR-API-KEY-32-CHARACTERS",
"openWeatherCity": "Osaka",
"openWeatherCountry": "JP",
"openWeatherCity": "",
"openWeatherCountry": "",
"clockDuration": "10000",
"weatherDuration": "5000",
"utcOffsetInSeconds": 32400,
"timeZone": "",
"weatherUnits": "metric"
}

View File

@@ -6,14 +6,29 @@
<title>ESPTimeCast Settings</title>
<style>
* { box-sizing: border-box; }
html{
background: radial-gradient(ellipse at 70% 0%, #2b425a 0%, #171e23 100%);
height: 100%;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
margin: 0; padding: 2rem 1rem;
background-color: #121212; color: #FFFFFF;
margin: 0;
padding: 2rem 1rem;
color: #FFFFFF;
background-repeat: no-repeat, repeat, repeat;
opacity: 0;
transition: opacity 0.6s cubic-bezier(.4,0,.2,1);
visibility: 0;
}
body.loaded {
visibility: visible;
opacity: 1;
}
body.modal-open {
overflow: hidden;
}
}
h1 {
text-align: center;
font-size: 1.5rem;
@@ -24,20 +39,30 @@
margin-top: 3rem;
margin-bottom: 0;
}
h2:first-of-type{
margin-top: 1.5rem;
}
.logo svg{
filter: drop-shadow(0px 0px 0.5rem gray);
filter: drop-shadow(0px 0px 0.5rem #1ec7fa);
width: 100%;
height: auto;
color: #c2f0ff;
margin: 0.5rem 0;
}
form {
display: flex; flex-direction: column;
max-width: 500px; margin: 0 auto;
background-color: #1e1e1e; padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 0 10px rgba(0,0,0,0.4);
background: linear-gradient(120deg, rgba(45,65,90,0.72) 0%, rgba(53,133,183,0.38) 100%);
padding: 1.5rem;
border-radius: 24px;
box-shadow: 0 10px 36px 0 rgba(40,170,255,0.11), 0 2px 8px 0 rgba(44, 70, 110, 0.08);
border: 1.5px solid rgba(180, 230, 255, 0.10)
}
label {
font-size: 0.9rem;
color: #bbb;
margin-bottom: 0.25rem;
margin-bottom: 0.25rem;
display: block;
margin-top: 0.75rem;
}
@@ -45,8 +70,10 @@
input[type="password"],
input[type="number"], select {
width: 100%; padding: 0.75rem;
border: none; border-radius: 8px;
background-color: #2c2c2c; color: #ffffff;
border: 1.5px solid rgba(180, 230, 255, 0.08);
border-radius: 8px;
background-color: rgba(225,245,255,0.07);
color: #ffffff;
font-size: 1rem; appearance: none;
}
input[type="submit"] {
@@ -69,7 +96,8 @@
flex: 1;
}
.primary-button {
background: linear-gradient(135deg, #007aff, #005ecb); color: white;
background: linear-gradient(90deg, #3e99bc, #47add4 85%);
color: white;
padding: 0.9rem; font-size: 1rem; font-weight: 600;
border: none; border-radius: 8px;
cursor: pointer; text-align: center;
@@ -85,22 +113,24 @@
.note {
font-size: 0.85rem;
text-align: center;
color: #888;
margin-top: 1rem;
}
#savingModal {
backdrop-filter: blur(5px);
position: fixed; top: 0; left: 0;
width: 100%; height: 100%;
background-color: rgba(18, 18, 18, 0.85);
background: radial-gradient(ellipse at 70% 0%, hsl(210.64deg 35.34% 26.08% / 60%) 0%, #171e2399 100%);
display: none; justify-content: center; align-items: center;
z-index: 1000;
}
#savingModalContent {
margin: 1.5rem; background-color: #1e1e1e;
padding: 2rem 2.5rem; border-radius: 12px;
text-align: center; color: white;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.6);
background: linear-gradient(120deg, rgb(45 65 90) 0%, rgb(53 133 183 / 44%) 100%);
border-radius: 12px;
box-shadow: 0 10px 36px 0 rgba(40, 170, 255, 0.11), 0 2px 8px 0 rgba(44, 70, 110, 0.08);
border: 1.5px solid rgba(180, 230, 255, 0.10);
margin: 1.5rem;
padding: 2rem 2.5rem;
text-align: center;
}
.spinner {
border: 4px solid rgba(255, 255, 255, 0.2);
@@ -113,12 +143,23 @@
.footer{
font-size: 0.8rem;
text-align: center;
color: #aaa;
margin-top: 1rem;
}
a{
color: white;
}
.small{
display: block;
font-size: 0.8rem;
margin-top: 0.25rem;
}
select option {
color: black;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
@@ -142,7 +183,7 @@
<label for="password">Password</label>
<div style="position: relative;">
<input type="password" id="password" name="password" required />
<label style="display: block; font-size: 0.8rem; color: #aaa; margin-top: 0.25rem;">
<label class="small">
<input type="checkbox" id="togglePassword" style="margin-right: 0.3rem;" />
Show Password
</label>
@@ -166,49 +207,102 @@
<option value="imperial">Imperial (°F)</option>
<option value="standard">Standard (K)</option>
</select>
<div class="small">
Consult the <a href="https://openweathermap.org/price" target="_blank" rel="noopener">OpenWeatherMap</a>
documendation for info about getting your API key, city, and country code.
</div>
<h2>Clock Settings</h2>
<label for="utcOffsetInHours">UTC Offset (default +09:00 for Japan)</label>
<select id="utcOffsetInHours" name="utcOffsetInHours" required>
<option value="-12">12:00 United States (Baker Island)</option>
<option value="-11">11:00 American Samoa, Niue</option>
<option value="-10">10:00 United States (Hawaii), French Polynesia</option>
<option value="-9.5">09:30 French Polynesia (Marquesas Islands)</option>
<option value="-9">09:00 United States (Alaska)</option>
<option value="-8">08:00 United States (California), Canada (British Columbia)</option>
<option value="-7">07:00 United States (Arizona), Mexico</option>
<option value="-6">06:00 United States (Central), Guatemala, Honduras</option>
<option value="-5">05:00 United States (Eastern), Colombia, Peru, Cuba</option>
<option value="-4.5">04:30 Venezuela</option>
<option value="-4">04:00 Bolivia, Paraguay, Canada (Atlantic)</option>
<option value="-3.5">03:30 Canada (Newfoundland)</option>
<option value="-3">03:00 Argentina, Brazil (East), Uruguay</option>
<option value="-2">02:00 South Georgia & South Sandwich Islands</option>
<option value="-1">01:00 Cape Verde, Azores (Portugal)</option>
<option value="0">±00:00 United Kingdom, Ireland, Portugal</option>
<option value="1">+01:00 Germany, France, Spain, Nigeria</option>
<option value="2">+02:00 South Africa, Egypt, Greece</option>
<option value="3">+03:00 Russia (West), Saudi Arabia, Kenya</option>
<option value="3.5">+03:30 Iran</option>
<option value="4">+04:00 United Arab Emirates, Azerbaijan</option>
<option value="4.5">+04:30 Afghanistan</option>
<option value="5">+05:00 Pakistan, Uzbekistan</option>
<option value="5.5">+05:30 India, Sri Lanka</option>
<option value="5.75">+05:45 Nepal</option>
<option value="6">+06:00 Bangladesh, Kazakhstan (East)</option>
<option value="6.5">+06:30 Myanmar</option>
<option value="7">+07:00 Thailand, Vietnam, Indonesia (West)</option>
<option value="8">+08:00 China, Malaysia, Singapore, Australia (West)</option>
<option value="8.75">+08:45 Australia (Eucla)</option>
<option value="9">+09:00 Japan, South Korea</option>
<option value="9.5">+09:30 Australia (Northern Territory, South Australia)</option>
<option value="10">+10:00 Australia (Queensland, New South Wales), Papua New Guinea</option>
<option value="10.5">+10:30 Australia (Lord Howe Island)</option>
<option value="11">+11:00 Solomon Islands, New Caledonia</option>
<option value="12">+12:00 New Zealand, Fiji</option>
<option value="12.75">+12:45 New Zealand (Chatham Islands)</option>
<option value="13">+13:00 Tonga, Samoa</option>
<option value="14">+14:00 Kiribati (Line Islands)</option>
<label for="timeZone">Time Zone</label>
<select id="timeZone" name="timeZone" required>
<option value="" disabled selected>Select your time zone</option>
<option value="Africa/Algiers">Africa/Algiers</option>
<option value="Africa/Cairo">Africa/Cairo</option>
<option value="Africa/Casablanca">Africa/Casablanca</option>
<option value="Africa/Johannesburg">Africa/Johannesburg</option>
<option value="Africa/Lagos">Africa/Lagos</option>
<option value="Africa/Nairobi">Africa/Nairobi</option>
<option value="America/Anchorage">America/Anchorage</option>
<option value="America/Argentina/Buenos_Aires">America/Argentina/Buenos_Aires</option>
<option value="America/Bogota">America/Bogota</option>
<option value="America/Caracas">America/Caracas</option>
<option value="America/Chicago">America/Chicago</option>
<option value="America/Denver">America/Denver</option>
<option value="America/Lima">America/Lima</option>
<option value="America/Los_Angeles">America/Los_Angeles</option>
<option value="America/Mexico_City">America/Mexico_City</option>
<option value="America/New_York">America/New_York</option>
<option value="America/Phoenix">America/Phoenix</option>
<option value="America/Santiago">America/Santiago</option>
<option value="America/Sao_Paulo">America/Sao_Paulo</option>
<option value="America/Toronto">America/Toronto</option>
<option value="America/Vancouver">America/Vancouver</option>
<option value="Asia/Almaty">Asia/Almaty</option>
<option value="Asia/Amman">Asia/Amman</option>
<option value="Asia/Baghdad">Asia/Baghdad</option>
<option value="Asia/Baku">Asia/Baku</option>
<option value="Asia/Bangkok">Asia/Bangkok</option>
<option value="Asia/Beirut">Asia/Beirut</option>
<option value="Asia/Dhaka">Asia/Dhaka</option>
<option value="Asia/Dubai">Asia/Dubai</option>
<option value="Asia/Ho_Chi_Minh">Asia/Ho_Chi_Minh</option>
<option value="Asia/Hong_Kong">Asia/Hong_Kong</option>
<option value="Asia/Jakarta">Asia/Jakarta</option>
<option value="Asia/Jerusalem">Asia/Jerusalem</option>
<option value="Asia/Kabul">Asia/Kabul</option>
<option value="Asia/Karachi">Asia/Karachi</option>
<option value="Asia/Kathmandu">Asia/Kathmandu</option>
<option value="Asia/Kolkata">Asia/Kolkata</option>
<option value="Asia/Kuala_Lumpur">Asia/Kuala_Lumpur</option>
<option value="Asia/Kuwait">Asia/Kuwait</option>
<option value="Asia/Manila">Asia/Manila</option>
<option value="Asia/Riyadh">Asia/Riyadh</option>
<option value="Asia/Seoul">Asia/Seoul</option>
<option value="Asia/Shanghai">Asia/Shanghai</option>
<option value="Asia/Singapore">Asia/Singapore</option>
<option value="Asia/Taipei">Asia/Taipei</option>
<option value="Asia/Tashkent">Asia/Tashkent</option>
<option value="Asia/Tehran">Asia/Tehran</option>
<option value="Asia/Tokyo">Asia/Tokyo</option>
<option value="Asia/Yangon">Asia/Yangon</option>
<option value="Australia/Adelaide">Australia/Adelaide</option>
<option value="Australia/Brisbane">Australia/Brisbane</option>
<option value="Australia/Melbourne">Australia/Melbourne</option>
<option value="Australia/Perth">Australia/Perth</option>
<option value="Australia/Sydney">Australia/Sydney</option>
<option value="Europe/Amsterdam">Europe/Amsterdam</option>
<option value="Europe/Athens">Europe/Athens</option>
<option value="Europe/Belgrade">Europe/Belgrade</option>
<option value="Europe/Berlin">Europe/Berlin</option>
<option value="Europe/Brussels">Europe/Brussels</option>
<option value="Europe/Bucharest">Europe/Bucharest</option>
<option value="Europe/Budapest">Europe/Budapest</option>
<option value="Europe/Copenhagen">Europe/Copenhagen</option>
<option value="Europe/Dublin">Europe/Dublin</option>
<option value="Europe/Helsinki">Europe/Helsinki</option>
<option value="Europe/Istanbul">Europe/Istanbul</option>
<option value="Europe/Kiev">Europe/Kiev</option>
<option value="Europe/Lisbon">Europe/Lisbon</option>
<option value="Europe/London">Europe/London</option>
<option value="Europe/Madrid">Europe/Madrid</option>
<option value="Europe/Moscow">Europe/Moscow</option>
<option value="Europe/Oslo">Europe/Oslo</option>
<option value="Europe/Paris">Europe/Paris</option>
<option value="Europe/Prague">Europe/Prague</option>
<option value="Europe/Riga">Europe/Riga</option>
<option value="Europe/Rome">Europe/Rome</option>
<option value="Europe/Sofia">Europe/Sofia</option>
<option value="Europe/Stockholm">Europe/Stockholm</option>
<option value="Europe/Tallinn">Europe/Tallinn</option>
<option value="Europe/Vienna">Europe/Vienna</option>
<option value="Europe/Vilnius">Europe/Vilnius</option>
<option value="Europe/Warsaw">Europe/Warsaw</option>
<option value="Europe/Zurich">Europe/Zurich</option>
<option value="Pacific/Auckland">Pacific/Auckland</option>
<option value="Pacific/Fiji">Pacific/Fiji</option>
<option value="Pacific/Guam">Pacific/Guam</option>
<option value="Pacific/Honolulu">Pacific/Honolulu</option>
<option value="Etc/UTC">Etc/UTC</option>
</select>
@@ -217,12 +311,12 @@
<div>
<label for="clockDuration">Clock Duration</label>
<input type="number" id="clockDuration" name="clockDuration" min="1" required />
<label style="display: block; font-size: 0.8rem; color: #aaa; margin-top: 0.25rem;">(Seconds)</label>
<label class="small">(Seconds)</label>
</div>
<div>
<label for="weatherDuration">Weather Duration</label>
<input type="number" id="weatherDuration" name="weatherDuration" min="1" required />
<label style="display: block; font-size: 0.8rem; color: #aaa; margin-top: 0.25rem;">(Seconds)</label>
<label class="small">(Seconds)</label>
</div>
</div>
<br><br><br>
@@ -239,6 +333,51 @@
let isSaving = false;
let isAPMode = false;
function ensureReloadButton(options = {}) {
let modalContent = document.getElementById('savingModalContent');
if (!modalContent) return;
let btn = document.getElementById('reloadButton');
if (!btn) {
btn = document.createElement('button');
btn.id = 'reloadButton';
btn.className = 'primary-button';
btn.style.display = 'inline-block';
btn.style.margin = '1rem 0.5rem 0 0';
modalContent.appendChild(btn);
}
btn.textContent = options.text || "Reload Page";
btn.onclick = options.onClick || (() => location.reload());
btn.style.display = 'inline-block';
return btn;
}
function ensureRestoreButton(options = {}) {
let modalContent = document.getElementById('savingModalContent');
if (!modalContent) return;
let btn = document.getElementById('restoreButton');
if (!btn) {
btn = document.createElement('button');
btn.id = 'restoreButton';
btn.className = 'primary-button';
btn.style.display = 'inline-block';
btn.style.margin = '1rem 0 0 0.5rem';
modalContent.appendChild(btn);
}
btn.textContent = options.text || "Restore Backup";
btn.onclick = options.onClick || restoreBackupConfig;
btn.style.display = 'inline-block';
return btn;
}
function removeReloadButton() {
let btn = document.getElementById('reloadButton');
if (btn && btn.parentNode) btn.parentNode.removeChild(btn);
}
function removeRestoreButton() {
let btn = document.getElementById('restoreButton');
if (btn && btn.parentNode) btn.parentNode.removeChild(btn);
}
window.onbeforeunload = function () {
if (isSaving) {
return "Settings are being saved. Leaving now may interrupt the process.";
@@ -250,7 +389,6 @@ window.onload = function () {
.then(response => response.json())
.then(data => {
isAPMode = (data.mode === "ap");
// console.log("✅ Config data:", data);
if (isAPMode) {
document.querySelector('.footer').style.display = 'none';
}
@@ -263,18 +401,52 @@ window.onload = function () {
document.getElementById('weatherUnits').value = data.weatherUnits || 'metric';
document.getElementById('clockDuration').value = (data.clockDuration || 10000) / 1000;
document.getElementById('weatherDuration').value = (data.weatherDuration || 5000) / 1000;
document.getElementById('utcOffsetInHours').value = (data.utcOffsetInSeconds || 0) / 3600;
// Auto-detect browser's timezone if not set in config
if (!data.timeZone) {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (
tz &&
document.getElementById('timeZone').querySelector(`[value="${tz}"]`)
) {
document.getElementById('timeZone').value = tz;
} else {
document.getElementById('timeZone').value = '';
}
} catch (e) {
document.getElementById('timeZone').value = '';
}
} else {
document.getElementById('timeZone').value = data.timeZone;
}
})
.catch(err => {
console.error('Failed to load config:', err);
showSavingModal("");
updateSavingModal("⚠️ Failed to load configuration.", false);
document.getElementById('restoreButton').style.display = 'inline-block';
// Show appropriate button for load error
removeReloadButton();
removeRestoreButton();
const errorMsg = (err.message || "").toLowerCase();
if (
errorMsg.includes("config corrupted") ||
errorMsg.includes("failed to write config") ||
errorMsg.includes("restore")
) {
ensureRestoreButton();
} else {
ensureReloadButton();
}
});
document.querySelector('html').style.height = 'unset';
document.body.classList.add('loaded');
};
function submitConfig(event) {
async function submitConfig(event) {
event.preventDefault();
isSaving = true;
@@ -286,10 +458,6 @@ function submitConfig(event) {
formData.set('clockDuration', clockDuration);
formData.set('weatherDuration', weatherDuration);
const utcOffsetInHours = parseFloat(formData.get('utcOffsetInHours')) || 0;
formData.set('utcOffsetInSeconds', Math.round(utcOffsetInHours * 3600));
formData.delete('utcOffsetInHours'); // backend expects only utcOffsetInSeconds
const params = new URLSearchParams();
for (const pair of formData.entries()) {
params.append(pair[0], pair[1]);
@@ -297,6 +465,17 @@ function submitConfig(event) {
showSavingModal("Saving...");
// Check AP mode status
let isAPMode = false;
try {
const apStatusResponse = await fetch('/ap_status');
const apStatusData = await apStatusResponse.json();
isAPMode = apStatusData.isAP;
} catch (error) {
console.error("Error fetching AP status:", error);
// Handle error appropriately (e.g., assume not in AP mode)
}
fetch('/save', {
method: 'POST',
body: params
@@ -309,56 +488,63 @@ function submitConfig(event) {
}
return response.json();
})
.then(json => {
isSaving = false;
if (isAPMode) {
updateSavingModal("✅ Settings saved successfully!<br><br><br>Rebooting the device now... ", false);
.then(json => {
isSaving = false;
removeReloadButton();
removeRestoreButton();
if (isAPMode) {
updateSavingModal("✅ Settings saved successfully!<br><br><br>Rebooting the device now... ", false);
setTimeout(() => {
document.getElementById('configForm').style.display = 'none';
// Optionally update modal text:
updateSavingModal("✅ All done!<br><br><br>You can now close this tab safely.", false);
}, 5000);
return;
setTimeout(() => {
document.querySelector('html').style.height = '100%';
document.getElementById('configForm').style.display = 'none';
updateSavingModal("✅ All done!<br><br><br>You can now close this tab safely.", false);
}, 5000);
return;
} else {
updateSavingModal("✅ Configuration saved successfully.<br><br><br>Device will reboot", false);
setTimeout(() => location.reload(), 3000);
}
})
.catch(err => {
isSaving = false;
} else {
updateSavingModal("✅ Configuration saved successfully.<br><br><br>Device will reboot", false);
setTimeout(() => location.reload(), 3000);
}
})
.catch(err => {
isSaving = false;
if (isAPMode && err.message.includes("Failed to fetch")) {
console.warn("Expected disconnect in AP mode after reboot.");
// Ensure modal is visible
showSavingModal(""); // Create or show modal if needed
updateSavingModal("✅ Settings saved successfully!<br><br><br>Rebooting the device now... ", false);
setTimeout(() => {
document.getElementById('configForm').style.display = 'none';
// Optionally update modal text:
updateSavingModal("✅ All done!<br><br><br>You can now close this tab safely.", false);
}, 5000);
return;
}
console.error('Save error:', err);
let friendlyMessage = "Something went wrong while saving the configuration.";
if (err.message.includes("Failed to fetch")) {
friendlyMessage = "⚠️ Cannot connect to the device.<br>Is it powered on and connected?";
}
updateSavingModal(`${friendlyMessage}<br><br>Details: ${err.message}`, false);
const restoreBtn = document.getElementById('restoreButton');
restoreBtn.textContent = "Reload Page";
restoreBtn.onclick = () => location.reload();
restoreBtn.style.display = 'inline-block';
});
if (isAPMode && err.message.includes("Failed to fetch")) {
console.warn("Expected disconnect in AP mode after reboot.");
showSavingModal("");
updateSavingModal("✅ Settings saved successfully!<br><br><br>Rebooting the device now... ", false);
setTimeout(() => {
document.getElementById('configForm').style.display = 'none';
updateSavingModal("✅ All done!<br><br><br>You can now close this tab safely.", false);
}, 5000);
removeReloadButton();
removeRestoreButton();
return;
}
console.error('Save error:', err);
let friendlyMessage = "⚠️ Something went wrong while saving the configuration.";
if (err.message.includes("Failed to fetch")) {
friendlyMessage = "⚠️ Cannot connect to the device.<br>Is it powered on and connected?";
}
updateSavingModal(`${friendlyMessage}<br><br>Details: ${err.message}`, false);
// Show only one action button, based on error content
removeReloadButton();
removeRestoreButton();
const errorMsg = (err.message || "").toLowerCase();
if (
errorMsg.includes("config corrupted") ||
errorMsg.includes("failed to write config") ||
errorMsg.includes("restore")
) {
ensureRestoreButton();
} else {
ensureReloadButton();
}
});
}
function showSavingModal(message) {
@@ -382,13 +568,21 @@ function showSavingModal(message) {
}
function updateSavingModal(message, showSpinner = false) {
document.getElementById('savingModalText').innerHTML = message;
let modalText = document.getElementById('savingModalText');
modalText.innerHTML = message;
document.querySelector('#savingModal .spinner').style.display = showSpinner ? 'block' : 'none';
// Remove reload/restore buttons if no longer needed
if (message.includes("saved successfully") || message.includes("Backup restored") || message.includes("All done!")) {
removeReloadButton();
removeRestoreButton();
}
}
function restoreBackupConfig() {
showSavingModal("Restoring backup...");
document.getElementById('restoreButton').remove();
removeReloadButton();
removeRestoreButton();
fetch('/restore', { method: 'POST' })
.then(response => {
@@ -398,12 +592,17 @@ function restoreBackupConfig() {
return response.json();
})
.then(data => {
updateSavingModal("✅ Backup restored! Reloading...");
updateSavingModal("✅ Backup restored! Device will now reboot.");
setTimeout(() => location.reload(), 1500);
})
.catch(err => {
console.error("Restore error:", err);
updateSavingModal(`❌ Failed to restore backup: ${err.message}`, false);
// Show only one button, for backup restore failures show reload.
removeReloadButton();
removeRestoreButton();
ensureReloadButton();
});
}

View File

@@ -104,7 +104,7 @@ MD_MAX72XX::fontType_t mFactory[] PROGMEM =
3, 62, 40, 56, // 98 - 'b'
3, 56, 68, 68, // 99 - 'c'
3, 56, 40, 62, // 100 - 'd'
3, 60, 44, 44, // 101 - 'e'
3, 124, 84, 68, // 101 - 'e'
3, 8, 62, 10, // 102 - 'f'
3, 56, 84, 116, // 103 - 'g'
3, 62, 8, 56, // 104 - 'h'
@@ -112,14 +112,14 @@ MD_MAX72XX::fontType_t mFactory[] PROGMEM =
2, 32, 58, // 106 - 'j'
3, 60, 16, 40, // 107 - 'k'
1, 60, // 108 - 'l'
3, 56, 24, 56, // 109 - 'm'
5, 124, 4, 124, 4, 120, // 109 - 'm'
3, 124, 4, 120, // 110 - 'n'
3, 56, 40, 56, // 111 - 'o'
3, 120, 40, 56, // 112 - 'p'
3, 56, 68, 56, // 111 - 'o'
3, 124, 20, 8, // 112 - 'p'
3, 56, 40, 120, // 113 - 'q'
3, 56, 8, 24, // 114 - 'r'
3, 72, 84, 36, // 115 - 's'
3, 11, 123, 11, // 116 - 't'
3, 4, 124, 4, // 116 - 't'
3, 56, 32, 56, // 117 - 'u'
3, 24, 32, 24, // 118 - 'v'
3, 56, 48, 56, // 119 - 'w'

109
tz_lookup.h Normal file
View File

@@ -0,0 +1,109 @@
#ifndef TZ_LOOKUP_H
#define TZ_LOOKUP_H
typedef struct {
const char* iana;
const char* posix;
} TimeZoneMapping;
const TimeZoneMapping tz_mappings[] = {
{"Etc/UTC", "UTC0"},
{"Europe/London", "GMT0BST,M3.5.0/1,M10.5.0/2"},
{"Europe/Dublin", "IST-1GMT0,M10.5.0,M3.5.0/1"},
{"Europe/Lisbon", "WET0WEST,M3.5.0/1,M10.5.0"},
{"Europe/Paris", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Berlin", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Rome", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Madrid", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Amsterdam", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Brussels", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Zurich", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Vienna", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Prague", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Warsaw", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Budapest", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Athens", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"Europe/Istanbul", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"Europe/Moscow", "MSK-3"},
{"Europe/Helsinki", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"Europe/Bucharest", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"Europe/Sofia", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"Europe/Kiev", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"Europe/Belgrade", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Copenhagen", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Stockholm", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Oslo", "CET-1CEST,M3.5.0/2,M10.5.0/3"},
{"Europe/Riga", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"Europe/Tallinn", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"Europe/Vilnius", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"Africa/Cairo", "EET-2"},
{"Africa/Johannesburg", "SAST-2"},
{"Africa/Lagos", "WAT-1"},
{"Africa/Nairobi", "EAT-3"},
{"Africa/Casablanca", "WET0"},
{"Africa/Algiers", "CET-1"},
{"Asia/Jerusalem", "IST-2IDT,M3.4.4/26,M10.5.0"},
{"Asia/Dubai", "GST-4"},
{"Asia/Kolkata", "IST-5:30"},
{"Asia/Karachi", "PKT-5"},
{"Asia/Dhaka", "BDT-6"},
{"Asia/Ho_Chi_Minh", "ICT-7"},
{"Asia/Bangkok", "ICT-7"},
{"Asia/Jakarta", "WIB-7"},
{"Asia/Singapore", "SGT-8"},
{"Asia/Kuala_Lumpur", "MYT-8"},
{"Asia/Shanghai", "CST-8"},
{"Asia/Hong_Kong", "HKT-8"},
{"Asia/Taipei", "CST-8"},
{"Asia/Seoul", "KST-9"},
{"Asia/Tokyo", "JST-9"},
{"Asia/Manila", "PHT-8"},
{"Asia/Yangon", "MMT-6:30"},
{"Asia/Kathmandu", "NPT-5:45"},
{"Asia/Almaty", "ALMT-6"},
{"Asia/Baku", "AZT-4AZST,M3.5.0/5,M10.5.0/6"},
{"Asia/Tashkent", "UZT-5"},
{"Asia/Tehran", "IRST-3:30IRDT,J79/24,J263/24"},
{"Asia/Baghdad", "AST-3"},
{"Asia/Riyadh", "AST-3"},
{"Asia/Kuwait", "AST-3"},
{"Asia/Amman", "EET-2EEST,J80/24,J273/1"},
{"Asia/Beirut", "EET-2EEST,M3.5.0/0,M10.5.0/0"},
{"Asia/Kabul", "AFT-4:30"},
{"Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3"},
{"Australia/Brisbane", "AEST-10"},
{"Australia/Perth", "AWST-8"},
{"Australia/Adelaide", "ACST-9:30ACDT,M10.1.0,M4.1.0/3"},
{"Australia/Melbourne", "AEST-10AEDT,M10.1.0,M4.1.0/3"},
{"Pacific/Auckland", "NZST-12NZDT,M9.5.0,M4.1.0/3"},
{"Pacific/Fiji", "FJT-12FJST,M11.2.0,M1/1"},
{"Pacific/Honolulu", "HST10"},
{"Pacific/Guam", "ChST-10"},
{"America/Anchorage", "AKDT9AKST,M3.2.0,M11.1.0"},
{"America/Los_Angeles", "PDT8PST,M3.2.0,M11.1.0"},
{"America/Vancouver", "PDT8PST,M3.2.0,M11.1.0"},
{"America/Denver", "MDT7MST,M3.2.0,M11.1.0"},
{"America/Phoenix", "MST7"},
{"America/Chicago", "CDT6CST,M3.2.0,M11.1.0"},
{"America/Mexico_City", "CDT6CST,M4.1.0,M10.5.0"},
{"America/Toronto", "EDT5EST,M3.2.0,M11.1.0"},
{"America/New_York", "EDT5EST,M3.2.0,M11.1.0"},
{"America/Caracas", "VET4"},
{"America/Bogota", "COT5"},
{"America/Lima", "PET5"},
{"America/Santiago", "CLST4CLT,M9.1.1,M4.2.7"},
{"America/Argentina/Buenos_Aires", "ART3"},
{"America/Sao_Paulo", "BRT3BRST,M10.3.0/0,M2.3.0/0"}
};
#define TZ_MAPPINGS_COUNT (sizeof(tz_mappings)/sizeof(tz_mappings[0]))
inline const char* ianaToPosix(const char* iana) {
for (size_t i = 0; i < TZ_MAPPINGS_COUNT; i++) {
if (strcmp(iana, tz_mappings[i].iana) == 0)
return tz_mappings[i].posix;
}
return "UTC0"; // fallback
}
#endif // TZ_LOOKUP_H