This commit is contained in:
MichaelDvP
2025-10-27 11:04:28 +01:00
42 changed files with 654 additions and 456 deletions

View File

@@ -7,14 +7,7 @@ on:
pull_request:
branches: dev
paths:
- '**.c'
- '**.cpp'
- '**.h'
- '**.hpp'
- '**.json'
- '**.py'
- '**.md'
- '.github/workflows/pr_check.yml'
- 'src/**'
jobs:
pre-release:

View File

@@ -1,12 +1,14 @@
# see https://github.com/marketplace/actions/sonarcloud-scan-for-c-and-c#usage
name: Sonar Check
permissions:
contents: read
on:
push:
branches:
- dev
# pull_request:
# types: [opened, synchronize, reopened]
paths:
- 'src/**'
jobs:
build:

View File

@@ -19,7 +19,7 @@ C = $(words $N)$(eval N := x $N)
ECHO = python3 $(I)/scripts/echo_progress.py --stepno=$C --nsteps=$T
endif
# determine number of parallel compiles based on OS
# Optimize parallel build configuration
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
EXTRA_CPPFLAGS = -D LINUX
@@ -29,7 +29,9 @@ ifeq ($(UNAME_S),Darwin)
EXTRA_CPPFLAGS = -D OSX -Wno-tautological-constant-out-of-range-compare
JOBS ?= $(shell sysctl -n hw.ncpu)
endif
MAKEFLAGS += -j $(JOBS) -l $(JOBS)
# Set optimal parallel build settings
MAKEFLAGS += -j$(JOBS) -l$(shell echo $$(($(JOBS) * 2)))
# $(info Number of jobs: $(JOBS))
@@ -72,16 +74,21 @@ DEFAULTS = -DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DE
OUTPUT := $(CURDIR)/$(TARGET)
SYMBOLS := $(CURDIR)/$(BUILD)/$(TARGET).out
CSOURCES := $(foreach dir,$(SOURCES),$(wildcard $(dir)/*.c))
CXXSOURCES := $(foreach dir,$(SOURCES),$(wildcard $(dir)/*.cpp))
# Optimize source discovery - use shell find for better performance
CSOURCES := $(shell find $(SOURCES) -name "*.c" 2>/dev/null)
CXXSOURCES := $(shell find $(SOURCES) -name "*.cpp" 2>/dev/null)
OBJS := $(patsubst %,$(BUILD)/%.o,$(basename $(CSOURCES)) $(basename $(CXXSOURCES)) )
DEPS := $(patsubst %,$(BUILD)/%.d,$(basename $(CSOURCES)) $(basename $(CXXSOURCES)) )
OBJS := $(patsubst %,$(BUILD)/%.o,$(basename $(CSOURCES)) $(basename $(CXXSOURCES)))
DEPS := $(patsubst %,$(BUILD)/%.d,$(basename $(CSOURCES)) $(basename $(CXXSOURCES)))
INCLUDE += $(addprefix -I,$(foreach dir,$(INCLUDES), $(wildcard $(dir))))
INCLUDE += $(addprefix -I,$(foreach dir,$(LIBRARIES),$(wildcard $(dir)/include)))
# Optimize include path discovery
INCLUDE_DIRS := $(shell find $(INCLUDES) -type d 2>/dev/null)
LIBRARY_INCLUDES := $(shell find $(LIBRARIES) -name "include" -type d 2>/dev/null)
INCLUDE += $(addprefix -I,$(INCLUDE_DIRS) $(LIBRARY_INCLUDES))
LDLIBS += $(addprefix -L,$(foreach dir,$(LIBRARIES),$(wildcard $(dir)/lib)))
# Optimize library path discovery
LIBRARY_DIRS := $(shell find $(LIBRARIES) -name "lib" -type d 2>/dev/null)
LDLIBS += $(addprefix -L,$(LIBRARY_DIRS))
#----------------------------------------------------------------------
# Compiler & Linker
@@ -99,9 +106,11 @@ CXX := /usr/bin/g++
#----------------------------------------------------------------------
CPPFLAGS += $(DEFINES) $(DEFAULTS) $(INCLUDE)
CPPFLAGS += -ggdb -g3 -MMD
CPPFLAGS += -flto=auto -fno-lto
CPPFLAGS += -flto=auto
CPPFLAGS += -Wall -Wextra -Werror -Wswitch-enum
CPPFLAGS += -Wno-unused-parameter -Wno-missing-braces -Wno-vla-cxx-extension
CPPFLAGS += -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -fno-threadsafe-statics
CPPFLAGS += -Os -DNDEBUG
CPPFLAGS += $(EXTRA_CPPFLAGS)
@@ -122,7 +131,8 @@ else
LD := $(CXX)
endif
#DEPFLAGS += -MF $(BUILD)/$*.d
# Dependency file generation
DEPFLAGS += -MF $(BUILD)/$*.d -MT $@
LINK.o = $(LD) $(LDFLAGS) $(LDLIBS) $^ -o $@
COMPILE.c = $(CC) $(C_STANDARD) $(CFLAGS) $(DEPFLAGS) -c $< -o $@
@@ -139,7 +149,10 @@ COMPILE.cpp = $(CXX) $(CXX_STANDARD) $(CXXFLAGS) $(DEPFLAGS) -c $< -o $@
.SUFFIXES:
.INTERMEDIATE:
.PRECIOUS: $(OBJS) $(DEPS)
.PHONY: all clean help
.PHONY: all clean help cppcheck run
# Enable second expansion for more flexible rules
.SECONDEXPANSION:
#----------------------------------------------------------------------
# Targets
@@ -154,7 +167,6 @@ $(OUTPUT): $(OBJS)
@mkdir -p $(@D)
@$(ECHO) Linking $@
$(LINK.o)
$(SYMBOLS.out)
$(BUILD)/%.o: %.c
@mkdir -p $(@D)
@@ -182,8 +194,15 @@ clean:
@$(RM) -rf $(BUILD) $(OUTPUT)
help:
@echo available targets: all run clean
@echo $(OUTPUT)
@echo "Available targets:"
@echo " all - Build the project (default)"
@echo " run - Build and run the executable"
@echo " clean - Remove build artifacts"
@echo " cppcheck - Run static analysis"
@echo " help - Show this help message"
@echo ""
@echo "Output: $(OUTPUT)"
@echo "Jobs: $(JOBS)"
-include $(DEPS)

View File

@@ -70,7 +70,7 @@ Visit [emsesp.org](https://docs.emsesp.org) for more details on how to install a
To chat with the community reach out on our [Discord Server](https://discord.gg/3J3GgnzpyT).
If you find an issue or have a request, see [here](https://docs.emsesp.org/Support/) on how to submit a bug report or feature request.
If you find an issue or have a request, see [how to request support](https://docs.emsesp.org/Support/) on how to submit a bug report or feature request.
## 🎥&nbsp; **Live Demo**
@@ -82,13 +82,19 @@ EMS-ESP is a project created by [proddy](https://github.com/proddy) and owned an
If you like **EMS-ESP**, please give it a ✨ on GitHub, or even better fork it and contribute. You can also offer a small donation. This is an open-source project maintained by volunteers, and your support is greatly appreciated.
## 📦&nbsp; **Building**
To build the web interface only, run `platformio run -e build_webUI`. This will install the necessary dependencies and build the web interface and also create the embedded code used need to build the firmware. You can run the web interface locally by going to the `interface` directory and running `pnpm standalone`.
To build the firmware, run `platformio run`. This will build the firmware for all ESP32 modules and place the binaries in the `build/firmware` folder. If you want to configure the build for a single platform create a local `pio_local.ni` file in the root directory (see example in `pio_local.ini_example`).
## 📢&nbsp; **Libraries used**
- [esp8266-react](https://github.com/rjwats/esp8266-react) by @rjwats for the core framework that provides the Web UI, which has been heavily modified
- [esp8266-react](https://github.com/rjwats/esp8266-react) originally by @rjwats for the core framework that provides the Web UI, which has been heavily modified
- [uuid-\*](https://github.com/nomis/mcu-uuid-console) from @nomis. The console, syslog, telnet and logging are based off these awesome open source libraries
- [ArduinoJson](https://github.com/bblanchon/ArduinoJson) for all the JSON processing
- [espMqttClient](https://github.com/bertmelis/espMqttClient) for the MQTT client
- [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) and [AsyncTCP](https://github.com/ESP32Async/AsyncTCP) for the Web server and TCP backends
- [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) and [AsyncTCP](https://github.com/ESP32Async/AsyncTCP) for the Web server
## 📜&nbsp; **License**

View File

@@ -635,8 +635,8 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@paralleldrive/cuid2@2.2.2':
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
@@ -3565,7 +3565,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
'@paralleldrive/cuid2@2.2.2':
'@paralleldrive/cuid2@2.3.1':
dependencies:
'@noble/hashes': 1.8.0
@@ -4714,7 +4714,7 @@ snapshots:
formidable@3.5.4:
dependencies:
'@paralleldrive/cuid2': 2.2.2
'@paralleldrive/cuid2': 2.3.1
dezalgo: 1.0.4
once: 1.4.0

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { ToastContainer, Zoom } from 'react-toastify';
import AppRouting from 'AppRouting';
@@ -8,7 +8,8 @@ import type { Locales } from 'i18n/i18n-types';
import { loadLocaleAsync } from 'i18n/i18n-util.async';
import { detectLocale, navigatorDetector } from 'typesafe-i18n/detectors';
const availableLocales = [
// Memoize available locales to prevent recreation on every render
const AVAILABLE_LOCALES = [
'de',
'en',
'it',
@@ -20,47 +21,59 @@ const availableLocales = [
'sv',
'tr',
'cz'
];
] as Locales[];
const App = () => {
const App = memo(() => {
const [wasLoaded, setWasLoaded] = useState(false);
const [locale, setLocale] = useState<Locales>('en');
useEffect(() => {
// determine locale, take from session if set other default to browser language
const browserLocale = detectLocale('en', availableLocales, navigatorDetector);
// Memoize locale initialization to prevent unnecessary re-runs
const initializeLocale = useCallback(async () => {
const browserLocale = detectLocale('en', AVAILABLE_LOCALES, navigatorDetector);
const newLocale = (localStorage.getItem('lang') || browserLocale) as Locales;
localStorage.setItem('lang', newLocale);
setLocale(newLocale);
void loadLocaleAsync(newLocale).then(() => setWasLoaded(true));
await loadLocaleAsync(newLocale);
setWasLoaded(true);
}, []);
useEffect(() => {
void initializeLocale();
}, [initializeLocale]);
// Memoize toast container props to prevent recreation
const toastContainerProps = useMemo(
() => ({
position: 'bottom-left' as const,
autoClose: 3000,
hideProgressBar: false,
newestOnTop: false,
closeOnClick: true,
rtl: false,
pauseOnFocusLoss: true,
draggable: false,
pauseOnHover: false,
transition: Zoom,
closeButton: false,
theme: 'dark' as const,
toastStyle: {
border: '1px solid #177ac9',
width: 'fit-content'
}
}),
[]
);
if (!wasLoaded) return null;
return (
<TypesafeI18n locale={locale}>
<CustomTheme>
<AppRouting />
<ToastContainer
position="bottom-left"
autoClose={3000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable={false}
pauseOnHover={false}
transition={Zoom}
closeButton={false}
theme="dark"
toastStyle={{
border: '1px solid #177ac9'
}}
/>
<ToastContainer {...toastContainerProps} />
</CustomTheme>
</TypesafeI18n>
);
};
});
export default App;

View File

@@ -1,10 +1,17 @@
import { memo } from 'react';
import type { FC } from 'react';
import { CssBaseline, ThemeProvider, responsiveFontSizes } from '@mui/material';
import {
CssBaseline,
ThemeProvider,
responsiveFontSizes,
tooltipClasses
} from '@mui/material';
import { createTheme } from '@mui/material/styles';
import type { RequiredChildrenProps } from 'utils';
// Memoize dialog style to prevent recreation
export const dialogStyle = {
'& .MuiDialog-paper': {
borderRadius: '8px',
@@ -12,8 +19,9 @@ export const dialogStyle = {
borderStyle: 'solid',
borderWidth: '1px'
}
};
} as const;
// Memoize theme creation to prevent recreation
const theme = responsiveFontSizes(
createTheme({
typography: {
@@ -30,15 +38,45 @@ const theme = responsiveFontSizes(
text: {
disabled: '#eee' // white
}
},
components: {
MuiListItemText: {
styleOverrides: {
primary: {
fontSize: 14
},
secondary: {
color: '#9e9e9e' // grey[500]
}
}
},
MuiTooltip: {
defaultProps: {
placement: 'top',
arrow: true
},
styleOverrides: {
tooltip: {
padding: '4px 8px',
fontSize: 10,
color: 'rgba(0, 0, 0, 0.87)',
backgroundColor: '#4caf50', // MUI success.main default color
boxShadow: '0px 2px 8px rgba(0, 0, 0, 0.15)',
[`& .${tooltipClasses.arrow}`]: {
color: '#4caf50'
}
}
}
}
}
})
);
const CustomTheme: FC<RequiredChildrenProps> = ({ children }) => (
const CustomTheme: FC<RequiredChildrenProps> = memo(({ children }) => (
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
);
));
export default CustomTheme;

View File

@@ -327,7 +327,7 @@ const CustomEntities = () => {
/>
)}
<Box mt={1} display="flex" flexWrap="wrap">
<Box mt={2} display="flex" flexWrap="wrap">
<Box flexGrow={1}>
{numChanges > 0 && (
<ButtonRow>

View File

@@ -14,6 +14,7 @@ import {
IconButton,
ToggleButton,
ToggleButtonGroup,
Tooltip,
Typography
} from '@mui/material';
@@ -206,7 +207,7 @@ const Dashboard = memo(() => {
// if its a device (parent node) and has entities
if (di.nodes?.length) {
return (
<span style={{ fontWeight: 'bold', fontSize: '14px' }}>
<span style={{ fontSize: '15px' }}>
<DeviceIcon type_id={di.t ?? 0} />
&nbsp;&nbsp;{showType(di.n, di.t)}
<span style={{ color: 'lightblue' }}>&nbsp;({di.nodes?.length})</span>
@@ -284,37 +285,44 @@ const Dashboard = memo(() => {
{data.nodes.length > 0 && (
<>
<Box
display="flex"
justifyContent="flex-end"
flexWrap="nowrap"
whiteSpace="nowrap"
>
<ToggleButtonGroup
color="primary"
size="small"
color="primary"
value={showAll}
exclusive
onChange={handleShowAll}
>
<ButtonTooltip title={LL.ALLVALUES()} arrow>
<ButtonTooltip title={LL.ALLVALUES()}>
<ToggleButton value={true}>
<UnfoldMoreIcon sx={{ fontSize: 18 }} />
</ToggleButton>
</ButtonTooltip>
<ButtonTooltip title={LL.COMPACT()} arrow>
<ButtonTooltip title={LL.COMPACT()}>
<ToggleButton value={false}>
<UnfoldLessIcon sx={{ fontSize: 18 }} />
</ToggleButton>
</ButtonTooltip>
</ToggleButtonGroup>
<ButtonTooltip title={LL.DASHBOARD_1()} arrow>
<HelpOutlineIcon color="primary" sx={{ ml: 1, fontSize: 20 }} />
</ButtonTooltip>
<Box
padding={1}
justifyContent="center"
flexDirection="column"
<Tooltip title={LL.DASHBOARD_1()}>
<HelpOutlineIcon
sx={{
borderRadius: 1,
border: '1px solid grey'
ml: 1,
mt: 1,
fontSize: 20,
verticalAlign: 'middle'
}}
>
color="primary"
/>
</Tooltip>
</Box>
<Box mt={1} justifyContent="center" flexDirection="column">
<IconContext.Provider
value={{
color: 'lightblue',

View File

@@ -1,8 +1,10 @@
import {
memo,
useCallback,
useContext,
useEffect,
useLayoutEffect,
useMemo,
useState
} from 'react';
import { IconContext } from 'react-icons';
@@ -75,7 +77,7 @@ import { DeviceEntityMask, DeviceType, DeviceValueUOM_s } from './types';
import type { Device, DeviceValue } from './types';
import { deviceValueItemValidation } from './validators';
const Devices = () => {
const Devices = memo(() => {
const { LL } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
@@ -141,7 +143,9 @@ const Devices = () => {
return left + (right - left < 400 ? 0 : 200);
};
const common_theme = useTheme({
const common_theme = useMemo(
() =>
useTheme({
BaseRow: `
font-size: 14px;
`,
@@ -163,11 +167,21 @@ const Devices = () => {
background-color: #177ac9;
}
`
});
}),
[]
);
const device_theme = useTheme([
const device_theme = useMemo(
() =>
useTheme([
common_theme,
{
BaseRow: `
font-size: 15px;
.td {
height: 28px;
}
`,
Table: `
--data-table-library_grid-template-columns: repeat(1, minmax(0, 1fr)) 130px;
`,
@@ -176,14 +190,21 @@ const Devices = () => {
padding: 8px;
`,
Row: `
font-weight: bold;
&:nth-of-type(odd) .td {
background-color: #303030;
},
&:hover .td {
background-color: #177ac9;
},
`
}
]);
]),
[common_theme]
);
const data_theme = useTheme([
const data_theme = useMemo(
() =>
useTheme([
common_theme,
{
Table: `
@@ -225,7 +246,9 @@ const Devices = () => {
}
`
}
]);
]),
[common_theme]
);
const getSortIcon = (state: State, sortKey: unknown) => {
if (state.sortKey === sortKey && state.reverse) {
@@ -324,8 +347,10 @@ const Devices = () => {
return sc;
};
const hasMask = (id: string, mask: number) =>
(parseInt(id.slice(0, 2), 16) & mask) === mask;
const hasMask = useCallback(
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
[]
);
const handleDownloadCsv = () => {
const deviceIndex = coreData.devices.findIndex(
@@ -510,6 +535,7 @@ const Devices = () => {
const renderCoreData = () => (
<>
<Box justifyContent="center" flexDirection="column">
<IconContext.Provider
value={{
color: 'lightblue',
@@ -559,6 +585,7 @@ const Devices = () => {
</Table>
)}
</IconContext.Provider>
</Box>
</>
);
@@ -574,12 +601,13 @@ const Devices = () => {
return;
}
const showDeviceValue = (dv: DeviceValue) => {
const showDeviceValue = useCallback((dv: DeviceValue) => {
setSelectedDeviceValue(dv);
setDeviceValueDialogOpen(true);
};
}, []);
const renderNameCell = (dv: DeviceValue) => (
const renderNameCell = useCallback(
(dv: DeviceValue) => (
<>
{dv.id.slice(2)}&nbsp;
{hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) && (
@@ -592,17 +620,22 @@ const Devices = () => {
<CommentsDisabledOutlinedIcon color="primary" sx={{ fontSize: 12 }} />
)}
</>
),
[hasMask]
);
const shown_data = onlyFav
? deviceData.nodes.filter(
const shown_data = useMemo(() => {
if (onlyFav) {
return deviceData.nodes.filter(
(dv: DeviceValue) =>
hasMask(dv.id, DeviceEntityMask.DV_FAVORITE) &&
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
)
: deviceData.nodes.filter((dv: DeviceValue) =>
);
}
return deviceData.nodes.filter((dv: DeviceValue) =>
dv.id.slice(2).toLowerCase().includes(search.toLowerCase())
);
}, [deviceData.nodes, onlyFav, search]);
const deviceIndex = coreData.devices.findIndex(
(d: Device) => d.id === device_select.state.id
@@ -795,6 +828,6 @@ const Devices = () => {
)}
</SectionContent>
);
};
});
export default Devices;

View File

@@ -73,7 +73,7 @@ const Help = () => {
divider={<Divider orientation="vertical" flexItem />}
sx={{
borderRadius: 3,
border: '2px solid grey',
border: '1px solid lightblue',
justifyContent: 'space-evenly',
alignItems: 'center'
}}
@@ -99,7 +99,7 @@ const Help = () => {
)}
{me.admin && (
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
<List>
<ListItem>
<ListItemButton
component="a"

View File

@@ -338,7 +338,7 @@ const Scheduler = () => {
/>
)}
<Box mt={1} display="flex" flexWrap="wrap">
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1}>
{numChanges !== 0 && (
<ButtonRow>

View File

@@ -105,8 +105,6 @@ const Sensors = () => {
color: #90CAF9;
.th {
border-bottom: 1px solid #565656;
}
.th {
height: 36px;
}
`,
@@ -491,7 +489,7 @@ const Sensors = () => {
/>
)}
{sensorData?.analog_enabled === true && me.admin && (
<Box mt={1} display="flex" flexWrap="wrap" justifyContent="flex-end">
<Box mt={2} display="flex" flexWrap="wrap" justifyContent="flex-end">
<Button
variant="outlined"
color="primary"

View File

@@ -17,6 +17,7 @@ import {
DialogActions,
DialogContent,
DialogTitle,
Divider,
List
} from '@mui/material';
@@ -74,9 +75,9 @@ const Settings = () => {
</Dialog>
);
const content = () => (
<>
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
return (
<SectionContent>
<List>
<ListMenuItem
icon={TuneIcon}
bgcolor="#134ba2"
@@ -143,7 +144,15 @@ const Settings = () => {
{renderFactoryResetDialog()}
<Box mt={2} display="flex" flexWrap="wrap">
<Divider />
<Box
mt={2}
display="flex"
justifyContent="flex-end"
flexWrap="nowrap"
whiteSpace="nowrap"
>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"
@@ -153,10 +162,8 @@ const Settings = () => {
{LL.FACTORY_RESET()}
</Button>
</Box>
</>
</SectionContent>
);
return <SectionContent>{content()}</SectionContent>;
};
export default Settings;

View File

@@ -253,7 +253,7 @@ const SystemStatus = () => {
return (
<>
<List sx={{ borderRadius: 3, border: '2px solid grey' }}>
<List>
<ListMenuItem
icon={BuildIcon}
bgcolor="#72caf9"

View File

@@ -19,9 +19,10 @@ import {
Grid,
IconButton,
Link,
List,
ListItem,
ListItemText,
Table,
TableBody,
TableCell,
TableRow,
Typography
} from '@mui/material';
@@ -41,6 +42,7 @@ import {
} from 'components';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { TranslationFunctions } from 'i18n/i18n-types';
import { prettyDateTime } from 'utils/time';
// Constants moved outside component to avoid recreation
@@ -86,7 +88,7 @@ const VersionInfoDialog = memo(
latestVersion?: VersionInfo;
latestDevVersion?: VersionInfo;
locale: string;
LL: any;
LL: TranslationFunctions;
onClose: () => void;
}) => {
if (showVersionInfo === 0) return null;
@@ -97,30 +99,69 @@ const VersionInfoDialog = memo(
return (
<Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
<DialogTitle>Version Information</DialogTitle>
<DialogTitle>{LL.FIRMWARE_VERSION_INFO()}</DialogTitle>
<DialogContent dividers>
<List dense>
<ListItem>
<ListItemText
primary={<span style={{ color: 'lightblue' }}>{LL.TYPE(0)}</span>}
secondary={isStable ? LL.STABLE() : LL.DEVELOPMENT()}
/>
</ListItem>
<ListItem>
<ListItemText
primary={<span style={{ color: 'lightblue' }}>{LL.VERSION()}</span>}
secondary={version?.name}
/>
</ListItem>
<Table size="small" sx={{ borderCollapse: 'collapse', minWidth: 0 }}>
<TableBody>
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13,
width: 90
}}
>
{LL.TYPE(0)}
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{isStable ? LL.STABLE() : LL.DEVELOPMENT()}
</TableCell>
</TableRow>
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
{LL.VERSION()}
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{version?.name}
</TableCell>
</TableRow>
{version?.published_at && (
<ListItem>
<ListItemText
primary={<span style={{ color: 'lightblue' }}>Release Date</span>}
secondary={prettyDateTime(locale, new Date(version.published_at))}
/>
</ListItem>
<TableRow sx={{ height: 24, borderBottom: 'none' }}>
<TableCell
component="th"
scope="row"
sx={{
color: 'lightblue',
borderBottom: 'none',
pr: 1,
py: 0.5,
fontSize: 13
}}
>
Build Date
</TableCell>
<TableCell sx={{ borderBottom: 'none', py: 0.5, fontSize: 13 }}>
{prettyDateTime(locale, new Date(version.published_at))}
</TableCell>
</TableRow>
)}
</List>
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button
@@ -159,7 +200,7 @@ const InstallDialog = memo(
latestDevVersion?: VersionInfo;
downloadOnly: boolean;
platform: string;
LL: any;
LL: TranslationFunctions;
onClose: () => void;
onInstall: (url: string) => void;
}) => {

View File

@@ -1,22 +1,7 @@
import { Tooltip, type TooltipProps, styled, tooltipClasses } from '@mui/material';
import { Tooltip, type TooltipProps } from '@mui/material';
export const ButtonTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip
{...props}
placement="top"
arrow
classes={{ ...(className && { popper: className }) }}
/>
))(({ theme }) => ({
[`& .${tooltipClasses.arrow}`]: {
color: theme.palette.success.main
},
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.success.main,
color: 'rgba(0, 0, 0, 0.87)',
boxShadow: theme.shadows[1],
fontSize: 10
}
}));
export const ButtonTooltip = ({ children, ...props }: TooltipProps) => (
<Tooltip {...props}>{children}</Tooltip>
);
export default ButtonTooltip;

View File

@@ -1,30 +1,20 @@
import type { FC } from 'react';
import { Divider, Paper } from '@mui/material';
import { Paper } from '@mui/material';
import type { RequiredChildrenProps } from 'utils';
interface SectionContentProps extends RequiredChildrenProps {
title?: string;
id?: string;
}
const SectionContent: FC<SectionContentProps> = (props) => {
const { children, title, id } = props;
const { children, id } = props;
return (
<Paper id={id} sx={{ p: 2, m: 2 }}>
{title && (
<Divider
sx={{
pb: 2,
borderColor: 'primary.main',
fontSize: 20,
color: 'primary.main'
}}
<Paper
id={id}
sx={{ p: 1.5, m: 1.5, borderRadius: 3, border: '1px solid rgb(65, 65, 65)' }}
>
{title}
</Divider>
)}
{children}
</Paper>
);

View File

@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import type { FC } from 'react';
import { useLocation } from 'react-router';
@@ -13,22 +13,26 @@ import { LayoutContext } from './context';
export const DRAWER_WIDTH = 210;
const Layout: FC<RequiredChildrenProps> = ({ children }) => {
const Layout: FC<RequiredChildrenProps> = memo(({ children }) => {
const [mobileOpen, setMobileOpen] = useState(false);
const [title, setTitle] = useState(PROJECT_NAME);
const { pathname } = useLocation();
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
// Memoize drawer toggle handler to prevent unnecessary re-renders
const handleDrawerToggle = useCallback(() => {
setMobileOpen((prev) => !prev);
}, []);
useEffect(() => setMobileOpen(false), [pathname]);
// Close drawer when route changes
useEffect(() => {
setMobileOpen(false);
}, [pathname]);
// cache the object to prevent unnecessary re-renders
const obj = useMemo(() => ({ title, setTitle }), [title]);
// Memoize context value to prevent unnecessary re-renders
const contextValue = useMemo(() => ({ title, setTitle }), [title]);
return (
<LayoutContext.Provider value={obj}>
<LayoutContext.Provider value={contextValue}>
<LayoutAppBar title={title} onToggleDrawer={handleDrawerToggle} />
<LayoutDrawer mobileOpen={mobileOpen} onClose={handleDrawerToggle} />
<Box component="main" sx={{ marginLeft: { md: `${DRAWER_WIDTH}px` } }}>
@@ -37,6 +41,6 @@ const Layout: FC<RequiredChildrenProps> = ({ children }) => {
</Box>
</LayoutContext.Provider>
);
};
});
export default Layout;

View File

@@ -73,19 +73,6 @@ const LayoutMenu = () => {
>
<ListItemText
primary={LL.MODULES()}
// secondary={
// LL.CUSTOMIZATIONS() +
// ', ' +
// LL.SCHEDULER() +
// ', ' +
// LL.CUSTOM_ENTITIES(0) +
// '...'
// }
// secondaryTypographyProps={{
// noWrap: true,
// fontSize: 12,
// color: menuOpen ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0.5)'
// }}
sx={{ my: 0 }}
slotProps={{
primary: {

View File

@@ -28,11 +28,47 @@ const LayoutMenuItem = ({
to={to}
disabled={disabled || false}
selected={selected}
sx={{
transition: 'all 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
transform: selected ? 'scale(1.02)' : 'scale(1)',
backgroundColor: selected ? 'rgba(144, 202, 249, 0.1)' : 'transparent',
borderRadius: '8px',
margin: '2px 8px',
'&:hover': {
backgroundColor: 'rgba(68, 82, 211, 0.39)',
transform: selected ? 'scale(1.02)' : 'scale(1.01)'
},
'&::before': {
content: '""',
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: selected ? '4px' : '0px',
backgroundColor: '#90caf9',
borderRadius: '0 2px 2px 0',
transition: 'width 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)'
}
}}
>
<ListItemIcon
sx={{
color: selected ? '#90caf9' : '#9e9e9e',
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
transform: selected ? 'scale(1.1)' : 'scale(1)',
transitionProperty: 'color, transform'
}}
>
<ListItemIcon sx={{ color: selected ? '#90caf9' : '#9e9e9e' }}>
<Icon />
</ListItemIcon>
<ListItemText sx={{ color: selected ? '#90caf9' : '#f5f5f5' }}>
<ListItemText
sx={{
color: selected ? '#90caf9' : '#f5f5f5',
transition: 'color 0.05s cubic-bezier(0.55, 0.085, 0.68, 0.53)',
// fontWeight: selected ? '600' : '400',
transitionProperty: 'color, font-weight'
}}
>
{label}
</ListItemText>
</ListItemButton>

View File

@@ -1,6 +1,8 @@
import { memo } from 'react';
import { Box, CircularProgress } from '@mui/material';
const LazyLoader = () => (
const LazyLoader = memo(() => (
<Box
display="flex"
justifyContent="center"
@@ -15,6 +17,6 @@ const LazyLoader = () => (
>
<CircularProgress size={40} />
</Box>
);
));
export default LazyLoader;

View File

@@ -352,7 +352,8 @@ const cz: Translation = {
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ sestavení',
INTERNET_CONNECTION_REQUIRED: 'Pro automatickou kontrolu a instalaci aktualizací je třeba internetové připojení',
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi'
SWITCH_RELEASE_TYPE: 'Přepnout na {0} verzi',
FIRMWARE_VERSION_INFO: 'Informace o verzi firmwaru'
};
export default cz;

View File

@@ -352,7 +352,8 @@ const de: Translation = {
PLATFORM: 'Plattform',
RELEASE_TYPE: 'Release Typ',
INTERNET_CONNECTION_REQUIRED: 'Für die automatische Versionsprüfung und Aktualisierung ist eine Internetverbindung erforderlich',
SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln'
SWITCH_RELEASE_TYPE: 'Zum {0}-Release wechseln',
FIRMWARE_VERSION_INFO: 'Firmware-Versionsinformation'
};
export default de;

View File

@@ -180,10 +180,10 @@ const en: Translation = {
DEVELOPMENT: 'Development',
UPTIME: 'System Uptime',
FREE_MEMORY: 'Free Memory',
PSRAM: 'PSRAM (Size / Free)',
FLASH: 'Flash Chip (Size , Speed)',
APPSIZE: 'Application (Partition: Used / Free)',
FILESYSTEM: 'File System (Used / Free)',
PSRAM: 'PSRAM (size / free)',
FLASH: 'Flash Chip (size , speed)',
APPSIZE: 'Application (partition: used / free)',
FILESYSTEM: 'File System (used / free)',
BUFFER_SIZE: 'Max Buffer Size',
COMPACT: 'Compact',
DOWNLOAD_SETTINGS_TEXT: 'Create a backup of your configuration and settings',
@@ -352,7 +352,8 @@ const en: Translation = {
PLATFORM: 'Platform',
RELEASE_TYPE: 'Release Type',
INTERNET_CONNECTION_REQUIRED: 'Internet connection required for automatic version checking and upgrading',
SWITCH_RELEASE_TYPE: 'Switch to {0} release'
SWITCH_RELEASE_TYPE: 'Switch to {0} release',
FIRMWARE_VERSION_INFO: 'Firmware Version Information'
};
export default en;

View File

@@ -352,7 +352,8 @@ const fr: Translation = {
PLATFORM: 'Plateforme',
RELEASE_TYPE: 'Type de version',
INTERNET_CONNECTION_REQUIRED: 'Connexion Internet requise pour la vérification automatique des versions et la mise à niveau',
SWITCH_RELEASE_TYPE: 'Passer à la version {0}'
SWITCH_RELEASE_TYPE: 'Passer à la version {0}',
FIRMWARE_VERSION_INFO: 'Informations sur la version du firmware'
};
export default fr;

View File

@@ -352,7 +352,8 @@ const it: Translation = {
PLATFORM: 'Piattaforma',
RELEASE_TYPE: 'Tipo di rilascio',
INTERNET_CONNECTION_REQUIRED: 'Connessione internet richiesta per il controllo automatico delle versioni e l\'aggiornamento',
SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio'
SWITCH_RELEASE_TYPE: 'Cambia in {0} rilascio',
FIRMWARE_VERSION_INFO: 'Informazioni sulla versione del firmware'
};
export default it;

View File

@@ -179,12 +179,12 @@ const nl: Translation = {
STABLE: 'Stable',
DEVELOPMENT: 'Development',
UPTIME: 'Systeem Uptime',
FREE_MEMORY: 'Free Memory',
PSRAM: 'PSRAM (Size / Free)',
FLASH: 'Flash Chip (Size , Speed)',
APPSIZE: 'Application (Partition: Used / Free)',
FILESYSTEM: 'File System (Used / Free)',
BUFFER_SIZE: 'Max Buffer Size',
FREE_MEMORY: 'Vrij geheugen',
PSRAM: 'PSRAM (grootte / vrij)',
FLASH: 'Flash Chip (grootte , snelheid)',
APPSIZE: 'Applicatie (partition: gebruikt / vrij)',
FILESYSTEM: 'Bestandssysteem (gebruikt / vrij)',
BUFFER_SIZE: 'Max buffer grootte',
COMPACT: 'Compact',
DOWNLOAD_SETTINGS_TEXT: 'Maak een back-up van uw configuratie en instellingen',
UPLOAD_TEXT: 'Upload een nieuw firmwarebestand (.bin) of een back-upbestand (.json)',
@@ -226,7 +226,7 @@ const nl: Translation = {
MQTT_INT_MIXER: 'Mixer Modules',
MQTT_INT_WATER: 'Water Modules',
MQTT_QUEUE: 'MQTT Queue',
DEFAULT: 'Default',
DEFAULT: 'Standaard',
MQTT_ENTITY_FORMAT: 'Entity ID formaat',
MQTT_ENTITY_FORMAT_0: 'Eén instantie, lange naam (v3.4)',
MQTT_ENTITY_FORMAT_1: 'Eén instantie, korte naam',
@@ -250,20 +250,20 @@ const nl: Translation = {
AP_PROVIDE_TEXT_3: 'nooit',
AP_PREFERRED_CHANNEL: 'Voorkeurskanaal',
AP_HIDE_SSID: 'SSID verbergen',
AP_CLIENTS: 'AP Clients',
AP_MAX_CLIENTS: 'Max Clients',
AP_LOCAL_IP: 'Local IP',
AP_CLIENTS: 'AP Gebruikers',
AP_MAX_CLIENTS: 'Max Gebruikers',
AP_LOCAL_IP: 'Lokale IP',
NETWORK_SCAN: 'Scan WiFi Netwerken',
IDLE: 'Idle',
IDLE: 'Inactief',
LOST: 'Verloren',
SCANNING: 'Scannen',
SCAN_AGAIN: 'Opnieuw scannen',
NETWORK_SCANNER: 'Netwerk Scanner',
NETWORK_SCANNER: 'Netwerk Scannen',
NETWORK_NO_WIFI: 'Geen WiFi netwerken gevonden',
NETWORK_BLANK_SSID: 'laat leeg om WiFi uit te schakelen',
NETWORK_BLANK_BSSID: 'laat leeg om alleen SSID te bebruiken',
TX_POWER: 'Tx Vermogen',
HOSTNAME: 'Hostname',
HOSTNAME: 'Hostnaam',
NETWORK_DISABLE_SLEEP: 'WiFi Sleep Mode uitzetten',
NETWORK_LOW_BAND: 'Lagere WiFi bandbreedte gebruiken',
NETWORK_USE_DNS: 'Activeer mDNS Service',
@@ -273,9 +273,9 @@ const nl: Translation = {
NETWORK_GATEWAY: 'Gateway',
NETWORK_SUBNET: 'Subnetmasker',
NETWORK_DNS: 'DNS Servers',
ADDRESS_OF: '{0} Address',
ADMINISTRATOR: 'Administrator',
GUEST: 'Gast',
ADDRESS_OF: '{0} Adres',
ADMINISTRATOR: 'Beheerder',
GUEST: 'Bezoeker',
NEW: 'Nieuwe',
NEW_NAME_OF: 'Hernoem {0}',
ENTITY: 'Entiteit',
@@ -352,7 +352,8 @@ const nl: Translation = {
PLATFORM: 'Platform',
RELEASE_TYPE: 'Release Typ',
INTERNET_CONNECTION_REQUIRED: 'Internetverbinding vereist voor automatische versiecontrole en -upgrade',
SWITCH_RELEASE_TYPE: 'Switch naar {0} release'
SWITCH_RELEASE_TYPE: 'Switch naar {0} release',
FIRMWARE_VERSION_INFO: 'Informatie over firmwareversie'
};
export default nl;

View File

@@ -352,7 +352,8 @@ const no: Translation = {
PLATFORM: 'Plattform',
RELEASE_TYPE: 'Utgivelses type',
INTERNET_CONNECTION_REQUIRED: 'Internettilkobling kreves for automatisk versjonskontroll og oppgradering',
SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse'
SWITCH_RELEASE_TYPE: 'Bytt til {0} utgivelse',
FIRMWARE_VERSION_INFO: 'Informasjon om firmwareversjon'
};
export default no;

View File

@@ -352,7 +352,8 @@ const pl: BaseTranslation = {
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ wydania',
INTERNET_CONNECTION_REQUIRED: 'Połączenie internetowe jest wymagane do automatycznej kontroli wersji i aktualizacji',
SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie'
SWITCH_RELEASE_TYPE: 'Zmień na {0} wydanie',
FIRMWARE_VERSION_INFO: 'Informacje o wersji firmware'
};
export default pl;

View File

@@ -352,7 +352,8 @@ const sk: Translation = {
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Typ vydania',
INTERNET_CONNECTION_REQUIRED: 'Internetové pripojenie je potrebné pre automatickú kontrolu a aktualizáciu',
SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu'
SWITCH_RELEASE_TYPE: 'Prepnúť na {0} verziu',
FIRMWARE_VERSION_INFO: 'Informácie o verzii firmware'
};
export default sk;

View File

@@ -352,7 +352,8 @@ const sv: Translation = {
PLATFORM: 'Plattform',
RELEASE_TYPE: 'Utgivelsestyp',
INTERNET_CONNECTION_REQUIRED: 'Internetanslutning krävs för automatisk version kontroll och uppdatering',
SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva'
SWITCH_RELEASE_TYPE: 'Byt till {0} utgåva',
FIRMWARE_VERSION_INFO: 'Information om firmwareversion'
};
export default sv;

View File

@@ -352,7 +352,8 @@ const tr: Translation = {
PLATFORM: 'Platforma',
RELEASE_TYPE: 'Sürüm Tipi',
INTERNET_CONNECTION_REQUIRED: 'Otomatik sürüm kontrolü ve güncelleme için internet bağlantısı gereklidir',
SWITCH_RELEASE_TYPE: '{0} sürümüne geç'
SWITCH_RELEASE_TYPE: '{0} sürümüne geç',
FIRMWARE_VERSION_INFO: 'Firmware Sürüm Bilgisi'
};
export default tr;

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { useBlocker } from 'react-router';
import { toast } from 'react-toastify';
@@ -35,20 +35,26 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
setDirtyFlags([]);
});
const updateDataValue = (new_data: D) => {
// Memoize updateDataValue to prevent unnecessary re-renders
const updateDataValue = useCallback(
(new_data: D) => {
updateData({ data: new_data });
};
},
[updateData]
);
const loadData = async () => {
// Memoize loadData to prevent unnecessary re-renders
const loadData = useCallback(async () => {
setDirtyFlags([]);
setErrorMessage(undefined);
await readData().catch((error: Error) => {
toast.error(error.message);
setErrorMessage(error.message);
});
};
}, [readData]);
const saveData = async () => {
// Memoize saveData to prevent unnecessary re-renders
const saveData = useCallback(async () => {
if (!data) {
return;
}
@@ -64,14 +70,15 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
setErrorMessage(error.message);
}
});
};
}, [data, writeData]);
return {
loadData,
saveData,
saving: saving as boolean,
updateDataValue,
data: data as D, // Explicitly define the type of 'data'
origData: origData as D, // Explicitly define the type of 'origData' to 'D'
data: data as D,
origData: origData as D,
dirtyFlags,
setDirtyFlags,
setOrigData,

View File

@@ -54,6 +54,7 @@
"skipDefaultLibCheck": true,
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo",
"assumeChangesOnlyAffectDirectDependencies": true,
// Path mapping for cleaner imports
"baseUrl": ".",

View File

@@ -281,11 +281,12 @@ export default defineConfig(
},
rollupOptions: {
// Enable tree shaking
// Enable aggressive tree shaking
treeshake: {
moduleSideEffects: false,
propertyReadSideEffects: false,
tryCatchDeoptimization: false
tryCatchDeoptimization: false,
unknownGlobalSideEffects: false
},
output: {
// Optimize chunk naming for better caching
@@ -301,6 +302,24 @@ export default defineConfig(
if (id.includes('preact')) {
return '@preact';
}
if (id.includes('@mui/material')) {
return '@mui-material';
}
if (id.includes('@mui/icons-material')) {
return '@mui-icons';
}
if (id.includes('alova')) {
return '@alova';
}
if (id.includes('typesafe-i18n')) {
return '@i18n';
}
if (id.includes('react-toastify')) {
return '@toastify';
}
if (id.includes('@table-library')) {
return '@table-library';
}
if (id.includes('uuid')) {
return '@uuid';
}
@@ -316,8 +335,14 @@ export default defineConfig(
if (id.includes('components/')) {
return 'components';
}
if (id.includes('pages/') || id.includes('routes/')) {
return 'pages';
if (id.includes('app/')) {
return 'app';
}
if (id.includes('utils/')) {
return 'utils';
}
if (id.includes('api/')) {
return 'api';
}
return undefined;
},

View File

@@ -84,8 +84,8 @@ packages:
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
engines: {node: ^14.21.3 || >=16}
'@paralleldrive/cuid2@2.2.2':
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
'@paralleldrive/cuid2@2.3.1':
resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==}
'@trivago/prettier-plugin-sort-imports@5.2.2':
resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==}
@@ -223,7 +223,7 @@ snapshots:
'@noble/hashes@1.8.0': {}
'@paralleldrive/cuid2@2.2.2':
'@paralleldrive/cuid2@2.3.1':
dependencies:
'@noble/hashes': 1.8.0
@@ -252,7 +252,7 @@ snapshots:
formidable@3.5.4:
dependencies:
'@paralleldrive/cuid2': 2.2.2
'@paralleldrive/cuid2': 2.3.1
dezalgo: 1.0.4
once: 1.4.0

View File

@@ -114,7 +114,9 @@ lib_deps =
[env:build_webUI]
platform = native
targets = build
extra_scripts = post:scripts/build_interface.py
extra_scripts = pre:scripts/build_interface.py
; Exclude all source files so the C code doesn't build
build_src_filter = -<*>
;
; Builds for different board types

View File

@@ -116,5 +116,7 @@ env.AddCustomTarget(
dependencies=None,
actions=[build_webUI],
title="build web interface",
description="installs pnpm packages, updates libraries and builds web UI"
description="installs pnpm packages, updates libraries and builds web UI",
always_build=True
)

View File

@@ -2,133 +2,121 @@ import hashlib
import shutil
import re
import os
from pathlib import Path
Import("env")
OUTPUT_DIR = "build{}".format(os.path.sep)
OUTPUT_DIR = Path("build")
def bin_copy(source, target, env):
"""Optimized firmware renaming and copying function."""
# get the application version from emsesp_version.h
bag = {}
exprs = [(re.compile(r'^#define EMSESP_APP_VERSION\s+"(\S+)"'), 'app_version')]
with open('./src/emsesp_version.h', 'r') as f:
for l in f.readlines():
for expr, var in exprs:
m = expr.match(l)
if m and len(m.groups()) > 0:
bag[var] = m.group(1)
# Get the application version from emsesp_version.h
version_file = Path('./src/emsesp_version.h')
if not version_file.exists():
print("Error: emsesp_version.h not found!")
return
app_version = bag.get('app_version')
app_version = None
version_pattern = re.compile(r'^#define EMSESP_APP_VERSION\s+"(\S+)"')
# print(env.Dump())
with version_file.open('r') as f:
for line in f:
match = version_pattern.match(line)
if match:
app_version = match.group(1)
break
# get the chip type, in uppercase
mcu = env.get('BOARD_MCU').upper()
# alternatively take platform from the pio target
# platform = str(target[0]).split(os.path.sep)[2]
if not app_version:
print("Error: Could not find EMSESP_APP_VERSION in emsesp_version.h!")
return
# work out the flash memory from the PIO env name (sloppy but works)
# unfortunately the board_upload.flash_size is not passed down
flash_mem = "4MB"
pio_env = env.get('PIOENV').upper()
# Get the chip type, in uppercase
mcu = env.get('BOARD_MCU', '').upper()
if not mcu:
print("Error: Could not determine MCU type!")
return
# Work out the flash memory from the PIO env name
flash_mem = "4MB" # default
pio_env = env.get('PIOENV', '').upper()
if pio_env:
parts = pio_env.split('_')
# if it ends with a _P skip (we use this to denote PSRAM)
if parts[-1].endswith("P"):
index = -2
else:
index = -1
# If it ends with _P skip (we use this to denote PSRAM)
index = -2 if parts[-1].endswith("P") else -1
# if doesn't have an M at the end
# If it has an M at the end, use it
if parts[index].endswith("M"):
flash_mem = parts[index] + "B"
# find if BOARD_HAS_PSRAM is in the cppdefines
cppdefines = env.get("CPPDEFINES")
if 'BOARD_HAS_PSRAM' in cppdefines:
psram = True
else:
psram = False
# Check if BOARD_HAS_PSRAM is in the cppdefines
cppdefines = env.get("CPPDEFINES", [])
psram = 'BOARD_HAS_PSRAM' in cppdefines
print("*********************************************")
print("EMS-ESP version: " + app_version)
print("=" * 90)
print(f"EMS-ESP version: {app_version}")
print(f"Has PSRAM: {'Yes' if psram else 'No'}")
print(f"MCU: {mcu}")
print(f"Flash Mem: {flash_mem}")
# show psram as Yes or No
psram_status = "Yes" if psram else "No"
print("Has PSRAM: " + psram_status)
print("MCU: "+str(mcu))
print("Flash Mem: " + flash_mem)
# convert . to _ so Windows doesn't complain
# Convert . to _ so Windows doesn't complain
# Format is EMS-ESP-<version>-<mcu>-<flash> with + at the end if it has PSRAM
variant = "EMS-ESP-" + \
app_version.replace(".", "_") + "-" + mcu + "-" + \
flash_mem + ("+" if psram else "")
variant = f"EMS-ESP-{app_version.replace('.', '_')}-{mcu}-{flash_mem}{'+' if psram else ''}"
# check if output directories exist and create if necessary
if not os.path.isdir(OUTPUT_DIR):
os.mkdir(OUTPUT_DIR)
# Create output directories
firmware_dir = OUTPUT_DIR / "firmware"
firmware_dir.mkdir(parents=True, exist_ok=True)
for d in ['firmware']:
if not os.path.isdir("{}{}".format(OUTPUT_DIR, d)):
os.mkdir("{}{}".format(OUTPUT_DIR, d))
# Define file paths
bin_file = firmware_dir / f"{variant}.bin"
md5_file = firmware_dir / f"{variant}.md5"
# create string with location and file names based on variant
bin_file = "{}firmware{}{}.bin".format(OUTPUT_DIR, os.path.sep, variant)
md5_file = "{}firmware{}{}.md5".format(OUTPUT_DIR, os.path.sep, variant)
# Remove existing files if they exist
for file_path in [bin_file, md5_file]:
if file_path.exists():
file_path.unlink()
# check if new target files exist and remove if necessary
for f in [bin_file]:
if os.path.isfile(f):
os.remove(f)
print(f"Filename: {bin_file}")
# check if new target files exist and remove if necessary
for f in [md5_file]:
if os.path.isfile(f):
os.remove(f)
# Copy firmware.bin to firmware/<variant>.bin
shutil.copy2(str(target[0]), str(bin_file))
print("Filename: "+bin_file)
# Calculate and write MD5 hash
with bin_file.open("rb") as f:
md5_hash = hashlib.md5(f.read()).hexdigest()
# copy firmware.bin to firmware/<variant>.bin
shutil.copy(str(target[0]), bin_file)
print(f"MD5: {md5_hash}")
md5_file.write_text(md5_hash)
with open(bin_file, "rb") as f:
result = hashlib.md5(f.read())
print("MD5: "+result.hexdigest())
file1 = open(md5_file, 'w')
file1.write(result.hexdigest())
file1.close()
# Make a copy using the old 3.6.x filename format for backwards compatibility
# Note: there is a chance newer E32V2s (which use the 16MB partition table and PSRAM)
# are running a custom build of the 3.6.5 firmware as 3.6.5 was released before
# production of the gateway board. Updating via the WebUI will break the system
# and require a manual update.
# make a copy using the old 3.6.x filename format for backwards compatibility with the WebUI version check, e.g.
# create a EMS-ESP-<version>-ESP32_S3.bin if target is s3_16M_P (16MB, PSRAM)
# create a EMS-ESP-<version>-ESP32.bin if target is s_4M (4MB, no PSRAM), compatible only with S32 V1 and E32 V1.0,1.4,1.5
#
# Note: there is a chance newer E32V2s (which use the 16MB partition table and PSRAM) are running a custom build
# of the 3.6.5 firmware as 3.6.5 was released before production of the gateway board. Updating via the WebUI will break the system and require a manual update.
#
extra_variant = ""
if env.get('PIOENV') == "s3_16M_P":
extra_variant = "EMS-ESP-" + \
app_version.replace(".", "_") + "-ESP32_S3"
elif env.get('PIOENV') == "s_4M":
extra_variant = "EMS-ESP-" + app_version.replace(".", "_") + "-ESP32"
pio_env = env.get('PIOENV', '')
extra_variant = None
if pio_env == "s3_16M_P":
extra_variant = f"EMS-ESP-{app_version.replace('.', '_')}-ESP32_S3"
elif pio_env == "s_4M":
extra_variant = f"EMS-ESP-{app_version.replace('.', '_')}-ESP32"
if extra_variant:
extra_bin_file = "{}firmware{}{}.bin".format(
OUTPUT_DIR, os.path.sep, extra_variant)
if os.path.isfile(extra_bin_file):
os.remove(extra_bin_file)
extra_bin_file = firmware_dir / f"{extra_variant}.bin"
extra_md5_file = firmware_dir / f"{extra_variant}.md5"
extra_md5_file = "{}firmware{}{}.md5".format(
OUTPUT_DIR, os.path.sep, extra_variant)
if os.path.isfile(extra_md5_file):
os.remove(extra_md5_file)
# Remove existing files if they exist
for file_path in [extra_bin_file, extra_md5_file]:
if file_path.exists():
file_path.unlink()
shutil.copy(bin_file, extra_bin_file)
shutil.copy(md5_file, extra_md5_file)
print("Filename copy for 3.6.x: "+extra_bin_file)
# Copy files
shutil.copy2(str(bin_file), str(extra_bin_file))
shutil.copy2(str(md5_file), str(extra_md5_file))
print(f"Filename copy for 3.6.x: {extra_bin_file}")
print("*********************************************")
print("=" * 90)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", [bin_copy])

View File

@@ -425,7 +425,7 @@ char * Helpers::render_string(char * result, const char * c, const uint8_t len)
c++;
p++;
}
*p = '\0'; // terminat result
*p = '\0'; // terminate result
return result;
}
@@ -467,7 +467,7 @@ char * Helpers::utf8tolatin1(char * result, const char * c, const uint8_t len) {
c++;
p++;
}
*p = '\0'; // terminat result
*p = '\0'; // terminate result
return result;
}
// creates string of hex values from an array of bytes

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.7.3-dev.22"
#define EMSESP_APP_VERSION "3.7.3-dev.23"