mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-06 07:49:52 +03:00
Compare commits
13 Commits
ea484e15f9
...
179ddcb348
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
179ddcb348 | ||
|
|
efa2c8fc4b | ||
|
|
c958c7d61a | ||
|
|
35fca9c450 | ||
|
|
61962fbc07 | ||
|
|
39e724befe | ||
|
|
43bb77b095 | ||
|
|
28e1e46586 | ||
|
|
2f5b879652 | ||
|
|
16930fe8ca | ||
|
|
1db1b6e524 | ||
|
|
43eba7a010 | ||
|
|
a837c9398c |
4
.github/workflows/pr_check.yml
vendored
4
.github/workflows/pr_check.yml
vendored
@@ -32,6 +32,6 @@ jobs:
|
|||||||
pip install wheel
|
pip install wheel
|
||||||
pip install -U platformio
|
pip install -U platformio
|
||||||
|
|
||||||
- name: Build native
|
- name: Build standalone
|
||||||
run: |
|
run: |
|
||||||
platformio run -e native
|
platformio run -e standalone
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"async-validator": "^4.2.5",
|
"async-validator": "^4.2.5",
|
||||||
"formidable": "^3.5.4",
|
"formidable": "^3.5.4",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"magic-string": "^0.30.19",
|
"magic-string": "^0.30.21",
|
||||||
"mime-types": "^3.0.1",
|
"mime-types": "^3.0.1",
|
||||||
"preact": "^10.27.2",
|
"preact": "^10.27.2",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
|
|||||||
28
interface/pnpm-lock.yaml
generated
28
interface/pnpm-lock.yaml
generated
@@ -39,8 +39,8 @@ importers:
|
|||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
magic-string:
|
magic-string:
|
||||||
specifier: ^0.30.19
|
specifier: ^0.30.21
|
||||||
version: 0.30.19
|
version: 0.30.21
|
||||||
mime-types:
|
mime-types:
|
||||||
specifier: ^3.0.1
|
specifier: ^3.0.1
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
@@ -663,8 +663,8 @@ packages:
|
|||||||
'@prefresh/utils@1.2.1':
|
'@prefresh/utils@1.2.1':
|
||||||
resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==}
|
resolution: {integrity: sha512-vq/sIuN5nYfYzvyayXI4C2QkprfNaHUQ9ZX+3xLD8nL3rWyzpxOm1+K7RtMbhd+66QcaISViK7amjnheQ/4WZw==}
|
||||||
|
|
||||||
'@prefresh/vite@2.4.10':
|
'@prefresh/vite@2.4.11':
|
||||||
resolution: {integrity: sha512-lt+ODASOtXRWaPplp7/DlrgAaInnQYNvcpCglQBMx2OeJPyZ4IqPRaxsK77w96mWshjYwkqTsRSHoAM7aAn0ow==}
|
resolution: {integrity: sha512-/XjURQqdRiCG3NpMmWqE9kJwrg9IchIOWHzulCfqg2sRe/8oQ1g5De7xrk9lbqPIQLn7ntBkKdqWXIj4E9YXyg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
preact: ^10.4.0 || ^11.0.0-0
|
preact: ^10.4.0 || ^11.0.0-0
|
||||||
vite: '>=2.0.0'
|
vite: '>=2.0.0'
|
||||||
@@ -1324,8 +1324,8 @@ packages:
|
|||||||
duplexer3@0.1.5:
|
duplexer3@0.1.5:
|
||||||
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
|
resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==}
|
||||||
|
|
||||||
electron-to-chromium@1.5.239:
|
electron-to-chromium@1.5.240:
|
||||||
resolution: {integrity: sha512-1y5w0Zsq39MSPmEjHjbizvhYoTaulVtivpxkp5q5kaPmQtsK6/2nvAzGRxNMS9DoYySp9PkW0MAQDwU1m764mg==}
|
resolution: {integrity: sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==}
|
||||||
|
|
||||||
emoji-regex@8.0.0:
|
emoji-regex@8.0.0:
|
||||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||||
@@ -2146,8 +2146,8 @@ packages:
|
|||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||||
|
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
make-dir@1.3.0:
|
make-dir@1.3.0:
|
||||||
resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==}
|
resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==}
|
||||||
@@ -3580,7 +3580,7 @@ snapshots:
|
|||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5)
|
'@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.5)
|
||||||
'@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.5)
|
'@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.5)
|
||||||
'@prefresh/vite': 2.4.10(preact@10.27.2)(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0))
|
'@prefresh/vite': 2.4.11(preact@10.27.2)(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0))
|
||||||
'@rollup/pluginutils': 4.2.1
|
'@rollup/pluginutils': 4.2.1
|
||||||
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.5)
|
babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.28.5)
|
||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
@@ -3599,7 +3599,7 @@ snapshots:
|
|||||||
|
|
||||||
'@prefresh/utils@1.2.1': {}
|
'@prefresh/utils@1.2.1': {}
|
||||||
|
|
||||||
'@prefresh/vite@2.4.10(preact@10.27.2)(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0))':
|
'@prefresh/vite@2.4.11(preact@10.27.2)(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@prefresh/babel-plugin': 0.5.2
|
'@prefresh/babel-plugin': 0.5.2
|
||||||
@@ -3995,7 +3995,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
baseline-browser-mapping: 2.8.20
|
baseline-browser-mapping: 2.8.20
|
||||||
caniuse-lite: 1.0.30001751
|
caniuse-lite: 1.0.30001751
|
||||||
electron-to-chromium: 1.5.239
|
electron-to-chromium: 1.5.240
|
||||||
node-releases: 2.0.26
|
node-releases: 2.0.26
|
||||||
update-browserslist-db: 1.1.4(browserslist@4.27.0)
|
update-browserslist-db: 1.1.4(browserslist@4.27.0)
|
||||||
|
|
||||||
@@ -4340,7 +4340,7 @@ snapshots:
|
|||||||
|
|
||||||
duplexer3@0.1.5: {}
|
duplexer3@0.1.5: {}
|
||||||
|
|
||||||
electron-to-chromium@1.5.239: {}
|
electron-to-chromium@1.5.240: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
|
|
||||||
@@ -5178,7 +5178,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
@@ -5967,7 +5967,7 @@ snapshots:
|
|||||||
vite-prerender-plugin@0.5.12(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0)):
|
vite-prerender-plugin@0.5.12(vite@7.1.12(@types/node@24.9.1)(terser@5.44.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
kolorist: 1.8.0
|
kolorist: 1.8.0
|
||||||
magic-string: 0.30.19
|
magic-string: 0.30.21
|
||||||
node-html-parser: 6.1.13
|
node-html-parser: 6.1.13
|
||||||
simple-code-frame: 1.3.0
|
simple-code-frame: 1.3.0
|
||||||
source-map: 0.7.6
|
source-map: 0.7.6
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { IconContext } from 'react-icons/lib';
|
import { IconContext } from 'react-icons/lib';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
@@ -44,7 +44,7 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
import { deviceValueItemValidation } from './validators';
|
import { deviceValueItemValidation } from './validators';
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = memo(() => {
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
const { me } = useContext(AuthenticatedContext);
|
const { me } = useContext(AuthenticatedContext);
|
||||||
|
|
||||||
@@ -163,9 +163,14 @@ const Dashboard = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const nodeIds = useMemo(
|
||||||
|
() => data.nodes.map((item: DashboardItem) => item.id),
|
||||||
|
[data.nodes]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
showAll
|
showAll
|
||||||
? tree.fns.onAddAll(data.nodes.map((item: DashboardItem) => item.id)) // expand tree
|
? tree.fns.onAddAll(nodeIds) // expand tree
|
||||||
: tree.fns.onRemoveAll(); // collapse tree
|
: tree.fns.onRemoveAll(); // collapse tree
|
||||||
}, [parentNodes]);
|
}, [parentNodes]);
|
||||||
|
|
||||||
@@ -195,27 +200,32 @@ const Dashboard = () => {
|
|||||||
[LL]
|
[LL]
|
||||||
);
|
);
|
||||||
|
|
||||||
const showName = (di: DashboardItem) => {
|
const showName = useCallback(
|
||||||
if (di.id < 100) {
|
(di: DashboardItem) => {
|
||||||
// if its a device (parent node) and has entities
|
if (di.id < 100) {
|
||||||
if (di.nodes?.length) {
|
// if its a device (parent node) and has entities
|
||||||
return (
|
if (di.nodes?.length) {
|
||||||
<span style={{ fontWeight: 'bold', fontSize: '14px' }}>
|
return (
|
||||||
<DeviceIcon type_id={di.t ?? 0} />
|
<span style={{ fontWeight: 'bold', fontSize: '14px' }}>
|
||||||
{showType(di.n, di.t)}
|
<DeviceIcon type_id={di.t ?? 0} />
|
||||||
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
{showType(di.n, di.t)}
|
||||||
</span>
|
<span style={{ color: 'lightblue' }}> ({di.nodes?.length})</span>
|
||||||
);
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (di.dv) {
|
||||||
if (di.dv) {
|
return <span>{di.dv.id.slice(2)}</span>;
|
||||||
return <span>{di.dv.id.slice(2)}</span>;
|
}
|
||||||
}
|
return null;
|
||||||
return null;
|
},
|
||||||
};
|
[showType]
|
||||||
|
);
|
||||||
|
|
||||||
const hasMask = (id: string, mask: number) =>
|
const hasMask = useCallback(
|
||||||
(parseInt(id.slice(0, 2), 16) & mask) === mask;
|
(id: string, mask: number) => (parseInt(id.slice(0, 2), 16) & mask) === mask,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const editDashboardValue = useCallback(
|
const editDashboardValue = useCallback(
|
||||||
(di: DashboardItem) => {
|
(di: DashboardItem) => {
|
||||||
@@ -237,6 +247,11 @@ const Dashboard = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasFavEntities = useMemo(
|
||||||
|
() => data.nodes.filter((item: DashboardItem) => item.id <= 90).length,
|
||||||
|
[data.nodes]
|
||||||
|
);
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (
|
return (
|
||||||
@@ -244,10 +259,6 @@ const Dashboard = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasFavEntities = data.nodes.filter(
|
|
||||||
(item: DashboardItem) => item.id <= 90
|
|
||||||
).length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!data.connected && (
|
{!data.connected && (
|
||||||
@@ -391,6 +402,6 @@ const Dashboard = () => {
|
|||||||
)}
|
)}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useContext, useEffect, useState } from 'react';
|
import { memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
|
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import CheckIcon from '@mui/icons-material/Done';
|
import CheckIcon from '@mui/icons-material/Done';
|
||||||
import DownloadIcon from '@mui/icons-material/GetApp';
|
import DownloadIcon from '@mui/icons-material/GetApp';
|
||||||
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -16,7 +17,11 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
Grid,
|
Grid,
|
||||||
|
IconButton,
|
||||||
Link,
|
Link,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
|
||||||
@@ -36,168 +41,143 @@ import {
|
|||||||
} from 'components';
|
} from 'components';
|
||||||
import { AuthenticatedContext } from 'contexts/authentication';
|
import { AuthenticatedContext } from 'contexts/authentication';
|
||||||
import { useI18nContext } from 'i18n/i18n-react';
|
import { useI18nContext } from 'i18n/i18n-react';
|
||||||
|
import { prettyDateTime } from 'utils/time';
|
||||||
|
|
||||||
const Version = () => {
|
// Constants moved outside component to avoid recreation
|
||||||
const { LL, locale } = useI18nContext();
|
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
|
||||||
const { me } = useContext(AuthenticatedContext);
|
const STABLE_RELNOTES_URL =
|
||||||
|
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
|
||||||
|
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
||||||
|
const DEV_RELNOTES_URL =
|
||||||
|
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
||||||
|
|
||||||
const [restarting, setRestarting] = useState<boolean>(false);
|
// Types for better type safety
|
||||||
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
|
interface VersionData {
|
||||||
const [usingDevVersion, setUsingDevVersion] = useState<boolean>(false);
|
emsesp_version: string;
|
||||||
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
|
arduino_version: string;
|
||||||
const [devUpgradeAvailable, setDevUpgradeAvailable] = useState<boolean>(false);
|
esp_platform: string;
|
||||||
const [stableUpgradeAvailable, setStableUpgradeAvailable] =
|
flash_chip_size: number;
|
||||||
useState<boolean>(false);
|
psram: boolean;
|
||||||
const [internetLive, setInternetLive] = useState<boolean>(false);
|
build_flags?: string;
|
||||||
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
|
}
|
||||||
|
|
||||||
const STABLE_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/';
|
interface UpgradeCheckData {
|
||||||
const STABLE_RELNOTES_URL =
|
emsesp_version: string;
|
||||||
'https://github.com/emsesp/EMS-ESP32/blob/main/CHANGELOG.md';
|
dev_upgradeable: boolean;
|
||||||
|
stable_upgradeable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const DEV_URL = 'https://github.com/emsesp/EMS-ESP32/releases/download/latest/';
|
interface VersionInfo {
|
||||||
const DEV_RELNOTES_URL =
|
name: string;
|
||||||
'https://github.com/emsesp/EMS-ESP32/blob/dev/CHANGELOG_LATEST.md';
|
published_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const { send: sendCheckUpgrade } = useRequest(
|
// Memoized components for better performance
|
||||||
(versions: string) => callAction({ action: 'checkUpgrade', param: versions }),
|
const VersionInfoDialog = memo(
|
||||||
{
|
({
|
||||||
immediate: false
|
showVersionInfo,
|
||||||
}
|
latestVersion,
|
||||||
).onSuccess((event) => {
|
latestDevVersion,
|
||||||
const data = event.data as {
|
locale,
|
||||||
emsesp_version: string;
|
LL,
|
||||||
dev_upgradeable: boolean;
|
onClose
|
||||||
stable_upgradeable: boolean;
|
}: {
|
||||||
};
|
showVersionInfo: number;
|
||||||
setDevUpgradeAvailable(data.dev_upgradeable);
|
latestVersion?: VersionInfo;
|
||||||
setStableUpgradeAvailable(data.stable_upgradeable);
|
latestDevVersion?: VersionInfo;
|
||||||
});
|
locale: string;
|
||||||
|
LL: any;
|
||||||
|
onClose: () => void;
|
||||||
|
}) => {
|
||||||
|
if (showVersionInfo === 0) return null;
|
||||||
|
|
||||||
const {
|
const isStable = showVersionInfo === 1;
|
||||||
data: data,
|
const version = isStable ? latestVersion : latestDevVersion;
|
||||||
send: loadData,
|
const relNotesUrl = isStable ? STABLE_RELNOTES_URL : DEV_RELNOTES_URL;
|
||||||
error
|
|
||||||
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
|
|
||||||
// older version of EMS-ESP using ESP32 (not S3) and no PSRAM, can't use OTA because of SSL support in HttpClient
|
|
||||||
if (event.data.arduino_version.startsWith('Tasmota')) {
|
|
||||||
setDownloadOnly(true);
|
|
||||||
}
|
|
||||||
setUsingDevVersion(event.data.emsesp_version.includes('dev'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const { send: sendUploadURL } = useRequest(
|
return (
|
||||||
(url: string) => callAction({ action: 'uploadURL', param: url }),
|
<Dialog sx={dialogStyle} open={showVersionInfo !== 0} onClose={onClose}>
|
||||||
{
|
<DialogTitle>Version Information</DialogTitle>
|
||||||
immediate: false
|
<DialogContent dividers>
|
||||||
}
|
<List dense>
|
||||||
);
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
// called immediately to get the latest versions on page load
|
primary={<span style={{ color: 'lightblue' }}>{LL.TYPE(0)}</span>}
|
||||||
const { data: latestVersion } = useRequest(getStableVersion);
|
secondary={isStable ? LL.STABLE() : LL.DEVELOPMENT()}
|
||||||
const { data: latestDevVersion } = useRequest(getDevVersion);
|
/>
|
||||||
|
</ListItem>
|
||||||
useEffect(() => {
|
<ListItem>
|
||||||
if (latestVersion && latestDevVersion) {
|
<ListItemText
|
||||||
sendCheckUpgrade(latestDevVersion.name + ',' + latestVersion.name)
|
primary={<span style={{ color: 'lightblue' }}>{LL.VERSION()}</span>}
|
||||||
.catch((error: Error) => {
|
secondary={version?.name}
|
||||||
toast.error('Failed to check for upgrades: ' + error.message);
|
/>
|
||||||
})
|
</ListItem>
|
||||||
.finally(() => {
|
{version?.published_at && (
|
||||||
setInternetLive(true);
|
<ListItem>
|
||||||
});
|
<ListItemText
|
||||||
}
|
primary={<span style={{ color: 'lightblue' }}>Release Date</span>}
|
||||||
}, [latestVersion, latestDevVersion]);
|
secondary={prettyDateTime(locale, new Date(version.published_at))}
|
||||||
|
/>
|
||||||
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
|
</ListItem>
|
||||||
const DIVISIONS: Array<{ amount: number; name: string }> = [
|
)}
|
||||||
{ amount: 60, name: 'seconds' },
|
</List>
|
||||||
{ amount: 60, name: 'minutes' },
|
</DialogContent>
|
||||||
{ amount: 24, name: 'hours' },
|
<DialogActions>
|
||||||
{ amount: 7, name: 'days' },
|
<Button
|
||||||
{ amount: 4.34524, name: 'weeks' },
|
variant="outlined"
|
||||||
{ amount: 12, name: 'months' },
|
component="a"
|
||||||
{ amount: Number.POSITIVE_INFINITY, name: 'years' }
|
href={relNotesUrl}
|
||||||
];
|
target="_blank"
|
||||||
function formatTimeAgo(date: Date) {
|
color="primary"
|
||||||
let duration = (date.getTime() - new Date().getTime()) / 1000;
|
>
|
||||||
for (let i = 0; i < DIVISIONS.length; i++) {
|
Changelog
|
||||||
const division = DIVISIONS[i];
|
</Button>
|
||||||
if (division && Math.abs(duration) < division.amount) {
|
<Button variant="outlined" onClick={onClose} color="secondary">
|
||||||
return rtf.format(
|
{LL.CLOSE()}
|
||||||
Math.round(duration),
|
</Button>
|
||||||
division.name as Intl.RelativeTimeFormatUnit
|
</DialogActions>
|
||||||
);
|
</Dialog>
|
||||||
}
|
);
|
||||||
if (division) {
|
|
||||||
duration /= division.amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return rtf.format(0, 'seconds');
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
const InstallDialog = memo(
|
||||||
immediate: false
|
({
|
||||||
});
|
openInstallDialog,
|
||||||
|
fetchDevVersion,
|
||||||
|
latestVersion,
|
||||||
|
latestDevVersion,
|
||||||
|
downloadOnly,
|
||||||
|
platform,
|
||||||
|
LL,
|
||||||
|
onClose,
|
||||||
|
onInstall
|
||||||
|
}: {
|
||||||
|
openInstallDialog: boolean;
|
||||||
|
fetchDevVersion: boolean;
|
||||||
|
latestVersion?: VersionInfo;
|
||||||
|
latestDevVersion?: VersionInfo;
|
||||||
|
downloadOnly: boolean;
|
||||||
|
platform: string;
|
||||||
|
LL: any;
|
||||||
|
onClose: () => void;
|
||||||
|
onInstall: (url: string) => void;
|
||||||
|
}) => {
|
||||||
|
const binURL = useMemo(() => {
|
||||||
|
if (!latestVersion || !latestDevVersion) return '';
|
||||||
|
|
||||||
const doRestart = async () => {
|
const version = fetchDevVersion ? latestDevVersion : latestVersion;
|
||||||
setRestarting(true);
|
const filename = `EMS-ESP-${version.name.replaceAll('.', '_')}-${platform}.bin`;
|
||||||
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
|
||||||
(error: Error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBinURL = (showingDev: boolean) => {
|
return fetchDevVersion
|
||||||
if (!internetLive) {
|
? `${DEV_URL}${filename}`
|
||||||
return '';
|
: `${STABLE_URL}v${version.name}/${filename}`;
|
||||||
}
|
}, [fetchDevVersion, latestVersion, latestDevVersion, platform]);
|
||||||
|
|
||||||
const filename =
|
|
||||||
'EMS-ESP-' +
|
|
||||||
(showingDev ? latestDevVersion.name : latestVersion.name).replaceAll(
|
|
||||||
'.',
|
|
||||||
'_'
|
|
||||||
) +
|
|
||||||
'-' +
|
|
||||||
getPlatform() +
|
|
||||||
'.bin';
|
|
||||||
return showingDev
|
|
||||||
? DEV_URL + filename
|
|
||||||
: STABLE_URL + 'v' + latestVersion.name + '/' + filename;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlatform = () => {
|
|
||||||
return (
|
|
||||||
[data.esp_platform, data.flash_chip_size >= 16384 ? '16MB' : '4MB'].join('-') +
|
|
||||||
(data.psram ? '+' : '')
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const installFirmwareURL = async (url: string) => {
|
|
||||||
await sendUploadURL(url).catch((error: Error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
});
|
|
||||||
setRestarting(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
useLayoutTitle('EMS-ESP Firmware');
|
|
||||||
|
|
||||||
const renderInstallDialog = () => {
|
|
||||||
const binURL = getBinURL(fetchDevVersion);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog sx={dialogStyle} open={openInstallDialog} onClose={onClose}>
|
||||||
sx={dialogStyle}
|
|
||||||
open={openInstallDialog}
|
|
||||||
onClose={() => closeInstallDialog()}
|
|
||||||
>
|
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{LL.UPDATE() +
|
{`${LL.UPDATE()} ${fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()} Firmware`}
|
||||||
' ' +
|
|
||||||
(fetchDevVersion ? LL.DEVELOPMENT() : LL.STABLE()) +
|
|
||||||
' Firmware'}
|
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Typography mb={2}>
|
<Typography mb={2}>
|
||||||
@@ -211,7 +191,7 @@ const Version = () => {
|
|||||||
<Button
|
<Button
|
||||||
startIcon={<CancelIcon />}
|
startIcon={<CancelIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => closeInstallDialog()}
|
onClick={onClose}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
>
|
>
|
||||||
{LL.CANCEL()}
|
{LL.CANCEL()}
|
||||||
@@ -219,7 +199,7 @@ const Version = () => {
|
|||||||
<Button
|
<Button
|
||||||
startIcon={<DownloadIcon />}
|
startIcon={<DownloadIcon />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => closeInstallDialog()}
|
onClick={onClose}
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
<Link underline="none" target="_blank" href={binURL} color="primary">
|
<Link underline="none" target="_blank" href={binURL} color="primary">
|
||||||
@@ -230,7 +210,7 @@ const Version = () => {
|
|||||||
<Button
|
<Button
|
||||||
startIcon={<WarningIcon color="warning" />}
|
startIcon={<WarningIcon color="warning" />}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => installFirmwareURL(binURL)}
|
onClick={() => onInstall(binURL)}
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
{LL.INSTALL()}
|
{LL.INSTALL()}
|
||||||
@@ -239,76 +219,185 @@ const Version = () => {
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const showFirmwareDialog = (useDevVersion: boolean) => {
|
// Helper function moved outside component
|
||||||
|
const getPlatform = (data: VersionData): string => {
|
||||||
|
return `${data.esp_platform}-${data.flash_chip_size >= 16384 ? '16MB' : '4MB'}${data.psram ? '+' : ''}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Version = () => {
|
||||||
|
const { LL, locale } = useI18nContext();
|
||||||
|
const { me } = useContext(AuthenticatedContext);
|
||||||
|
|
||||||
|
// State management
|
||||||
|
const [restarting, setRestarting] = useState<boolean>(false);
|
||||||
|
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
|
||||||
|
const [usingDevVersion, setUsingDevVersion] = useState<boolean>(false);
|
||||||
|
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
|
||||||
|
const [devUpgradeAvailable, setDevUpgradeAvailable] = useState<boolean>(false);
|
||||||
|
const [stableUpgradeAvailable, setStableUpgradeAvailable] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [internetLive, setInternetLive] = useState<boolean>(false);
|
||||||
|
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
|
||||||
|
const [showVersionInfo, setShowVersionInfo] = useState<number>(0);
|
||||||
|
|
||||||
|
// API calls with optimized error handling
|
||||||
|
const { send: sendCheckUpgrade } = useRequest(
|
||||||
|
(versions: string) => callAction({ action: 'checkUpgrade', param: versions }),
|
||||||
|
{ immediate: false }
|
||||||
|
).onSuccess((event) => {
|
||||||
|
const data = event.data as UpgradeCheckData;
|
||||||
|
setDevUpgradeAvailable(data.dev_upgradeable);
|
||||||
|
setStableUpgradeAvailable(data.stable_upgradeable);
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
send: loadData,
|
||||||
|
error
|
||||||
|
} = useRequest(SystemApi.readSystemStatus).onSuccess((event) => {
|
||||||
|
const systemData = event.data as VersionData;
|
||||||
|
if (systemData.arduino_version.startsWith('Tasmota')) {
|
||||||
|
setDownloadOnly(true);
|
||||||
|
}
|
||||||
|
setUsingDevVersion(systemData.emsesp_version.includes('dev'));
|
||||||
|
});
|
||||||
|
|
||||||
|
const { send: sendUploadURL } = useRequest(
|
||||||
|
(url: string) => callAction({ action: 'uploadURL', param: url }),
|
||||||
|
{ immediate: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: latestVersion } = useRequest(getStableVersion);
|
||||||
|
const { data: latestDevVersion } = useRequest(getDevVersion);
|
||||||
|
|
||||||
|
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
|
||||||
|
immediate: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Memoized values
|
||||||
|
const platform = useMemo(() => (data ? getPlatform(data) : ''), [data]);
|
||||||
|
const isDev = useMemo(
|
||||||
|
() => data?.emsesp_version.includes('dev') ?? false,
|
||||||
|
[data?.emsesp_version]
|
||||||
|
);
|
||||||
|
|
||||||
|
const doRestart = useCallback(async () => {
|
||||||
|
setRestarting(true);
|
||||||
|
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
|
||||||
|
(error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [sendAPI]);
|
||||||
|
|
||||||
|
const installFirmwareURL = useCallback(
|
||||||
|
async (url: string) => {
|
||||||
|
await sendUploadURL(url).catch((error: Error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
setRestarting(true);
|
||||||
|
},
|
||||||
|
[sendUploadURL]
|
||||||
|
);
|
||||||
|
|
||||||
|
const showFirmwareDialog = useCallback((useDevVersion: boolean) => {
|
||||||
setFetchDevVersion(useDevVersion);
|
setFetchDevVersion(useDevVersion);
|
||||||
setOpenInstallDialog(true);
|
setOpenInstallDialog(true);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const closeInstallDialog = () => {
|
const closeInstallDialog = useCallback(() => {
|
||||||
setOpenInstallDialog(false);
|
setOpenInstallDialog(false);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const showButtons = (showingDev: boolean) => {
|
const handleVersionInfoClose = useCallback(() => {
|
||||||
const choice = showingDev
|
setShowVersionInfo(0);
|
||||||
? !usingDevVersion
|
}, []);
|
||||||
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
|
|
||||||
: devUpgradeAvailable
|
// Effect for checking upgrades
|
||||||
? LL.UPDATE_AVAILABLE()
|
useEffect(() => {
|
||||||
: undefined
|
if (latestVersion && latestDevVersion) {
|
||||||
: usingDevVersion
|
const versions = `${latestDevVersion.name},${latestVersion.name}`;
|
||||||
? LL.SWITCH_RELEASE_TYPE(LL.STABLE())
|
sendCheckUpgrade(versions)
|
||||||
: stableUpgradeAvailable
|
.catch((error: Error) => {
|
||||||
? LL.UPDATE_AVAILABLE()
|
toast.error(`Failed to check for upgrades: ${error.message}`);
|
||||||
: undefined;
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setInternetLive(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [latestVersion, latestDevVersion, sendCheckUpgrade]);
|
||||||
|
|
||||||
|
useLayoutTitle('EMS-ESP Firmware');
|
||||||
|
|
||||||
|
// Memoized button rendering logic
|
||||||
|
const showButtons = useCallback(
|
||||||
|
(showingDev: boolean) => {
|
||||||
|
const choice = showingDev
|
||||||
|
? !usingDevVersion
|
||||||
|
? LL.SWITCH_RELEASE_TYPE(LL.DEVELOPMENT())
|
||||||
|
: devUpgradeAvailable
|
||||||
|
? LL.UPDATE_AVAILABLE()
|
||||||
|
: undefined
|
||||||
|
: usingDevVersion
|
||||||
|
? LL.SWITCH_RELEASE_TYPE(LL.STABLE())
|
||||||
|
: stableUpgradeAvailable
|
||||||
|
? LL.UPDATE_AVAILABLE()
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (!choice) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CheckIcon
|
||||||
|
color="success"
|
||||||
|
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
|
||||||
|
/>
|
||||||
|
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
|
||||||
|
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
sx={{ ml: 2 }}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => showFirmwareDialog(showingDev)}
|
||||||
|
>
|
||||||
|
{LL.REINSTALL()}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!me.admin) return null;
|
||||||
|
|
||||||
if (!choice) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Button
|
||||||
<CheckIcon
|
sx={{ ml: 2 }}
|
||||||
color="success"
|
variant="outlined"
|
||||||
sx={{ verticalAlign: 'middle', ml: 0.5, mr: 0.5 }}
|
color={choice === LL.UPDATE_AVAILABLE() ? 'success' : 'warning'}
|
||||||
/>
|
size="small"
|
||||||
<span style={{ color: '#66bb6a', fontSize: '0.8em' }}>
|
onClick={() => showFirmwareDialog(showingDev)}
|
||||||
{LL.LATEST_VERSION(usingDevVersion ? LL.DEVELOPMENT() : LL.STABLE())}
|
>
|
||||||
</span>
|
{choice}
|
||||||
<Button
|
</Button>
|
||||||
sx={{ ml: 2 }}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onClick={() => showFirmwareDialog(showingDev)}
|
|
||||||
>
|
|
||||||
{LL.REINSTALL()}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
[
|
||||||
|
usingDevVersion,
|
||||||
|
devUpgradeAvailable,
|
||||||
|
stableUpgradeAvailable,
|
||||||
|
me.admin,
|
||||||
|
LL,
|
||||||
|
showFirmwareDialog
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
if (!me.admin) {
|
const content = useMemo(() => {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
sx={{ ml: 2 }}
|
|
||||||
variant="outlined"
|
|
||||||
color={choice === LL.UPDATE_AVAILABLE() ? 'success' : 'warning'}
|
|
||||||
size="small"
|
|
||||||
onClick={() => showFirmwareDialog(showingDev)}
|
|
||||||
>
|
|
||||||
{choice}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const content = () => {
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
return <FormLoader onRetry={loadData} errorMessage={error?.message || ''} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isDev = data.emsesp_version.includes('dev');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box p={2} border="1px solid grey" borderRadius={2}>
|
<Box p={2} border="1px solid grey" borderRadius={2}>
|
||||||
@@ -344,7 +433,7 @@ const Version = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
<Grid size={{ xs: 8, md: 10 }}>
|
||||||
<Typography>
|
<Typography>
|
||||||
{getPlatform()}
|
{platform}
|
||||||
<Typography variant="caption">
|
<Typography variant="caption">
|
||||||
(
|
(
|
||||||
{data.psram ? (
|
{data.psram ? (
|
||||||
@@ -436,15 +525,10 @@ const Version = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
<Grid size={{ xs: 8, md: 10 }}>
|
||||||
<Typography>
|
<Typography>
|
||||||
<Link target="_blank" href={STABLE_RELNOTES_URL} color="primary">
|
{latestVersion?.name}
|
||||||
{latestVersion.name}
|
<IconButton onClick={() => setShowVersionInfo(1)}>
|
||||||
</Link>
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
{latestVersion.published_at && (
|
</IconButton>
|
||||||
<Typography component="span" variant="caption">
|
|
||||||
(
|
|
||||||
{formatTimeAgo(new Date(latestVersion.published_at))})
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{showButtons(false)}
|
{showButtons(false)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -454,15 +538,10 @@ const Version = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
<Grid size={{ xs: 8, md: 10 }}>
|
<Grid size={{ xs: 8, md: 10 }}>
|
||||||
<Typography>
|
<Typography>
|
||||||
<Link target="_blank" href={DEV_RELNOTES_URL} color="primary">
|
{latestDevVersion?.name}
|
||||||
{latestDevVersion.name}
|
<IconButton onClick={() => setShowVersionInfo(2)}>
|
||||||
</Link>
|
<InfoOutlinedIcon color="primary" sx={{ fontSize: 18 }} />
|
||||||
{latestDevVersion.published_at && (
|
</IconButton>
|
||||||
<Typography component="span" variant="caption">
|
|
||||||
(
|
|
||||||
{formatTimeAgo(new Date(latestDevVersion.published_at))})
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{showButtons(true)}
|
{showButtons(true)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -476,7 +555,25 @@ const Version = () => {
|
|||||||
)}
|
)}
|
||||||
{me.admin && (
|
{me.admin && (
|
||||||
<>
|
<>
|
||||||
{renderInstallDialog()}
|
<VersionInfoDialog
|
||||||
|
showVersionInfo={showVersionInfo}
|
||||||
|
latestVersion={latestVersion}
|
||||||
|
latestDevVersion={latestDevVersion}
|
||||||
|
locale={locale}
|
||||||
|
LL={LL}
|
||||||
|
onClose={handleVersionInfoClose}
|
||||||
|
/>
|
||||||
|
<InstallDialog
|
||||||
|
openInstallDialog={openInstallDialog}
|
||||||
|
fetchDevVersion={fetchDevVersion}
|
||||||
|
latestVersion={latestVersion}
|
||||||
|
latestDevVersion={latestDevVersion}
|
||||||
|
downloadOnly={downloadOnly}
|
||||||
|
platform={platform}
|
||||||
|
LL={LL}
|
||||||
|
onClose={closeInstallDialog}
|
||||||
|
onInstall={installFirmwareURL}
|
||||||
|
/>
|
||||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||||
{LL.UPLOAD()}
|
{LL.UPLOAD()}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -486,11 +583,30 @@ const Version = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
}, [
|
||||||
|
data,
|
||||||
|
error,
|
||||||
|
loadData,
|
||||||
|
LL,
|
||||||
|
platform,
|
||||||
|
isDev,
|
||||||
|
internetLive,
|
||||||
|
latestVersion,
|
||||||
|
latestDevVersion,
|
||||||
|
showVersionInfo,
|
||||||
|
locale,
|
||||||
|
openInstallDialog,
|
||||||
|
fetchDevVersion,
|
||||||
|
downloadOnly,
|
||||||
|
me.admin,
|
||||||
|
showButtons,
|
||||||
|
handleVersionInfoClose,
|
||||||
|
closeInstallDialog,
|
||||||
|
installFirmwareURL,
|
||||||
|
doRestart
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return <SectionContent>{restarting ? <SystemMonitor /> : content}</SectionContent>;
|
||||||
<SectionContent>{restarting ? <SystemMonitor /> : content()}</SectionContent>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Version;
|
export default memo(Version);
|
||||||
|
|||||||
@@ -1,18 +1,135 @@
|
|||||||
const LOCALE_FORMAT = new Intl.DateTimeFormat([...window.navigator.languages], {
|
// Cache for formatters to avoid recreation
|
||||||
day: 'numeric',
|
const formatterCache = new Map<string, Intl.DateTimeFormat>();
|
||||||
month: 'short',
|
const rtfCache = new Map<string, Intl.RelativeTimeFormat>();
|
||||||
year: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
second: 'numeric',
|
|
||||||
hour12: false
|
|
||||||
});
|
|
||||||
|
|
||||||
export const formatDateTime = (dateTime: string) =>
|
// Pre-computed time divisions for relative time formatting
|
||||||
LOCALE_FORMAT.format(new Date(dateTime.substring(0, 19)));
|
const TIME_DIVISIONS = [
|
||||||
|
{ amount: 60, name: 'seconds' as const },
|
||||||
|
{ amount: 60, name: 'minutes' as const },
|
||||||
|
{ amount: 24, name: 'hours' as const },
|
||||||
|
{ amount: 7, name: 'days' as const },
|
||||||
|
{ amount: 4.34524, name: 'weeks' as const },
|
||||||
|
{ amount: 12, name: 'months' as const },
|
||||||
|
{ amount: Number.POSITIVE_INFINITY, name: 'years' as const }
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const formatLocalDateTime = (date: Date) =>
|
/**
|
||||||
new Date(date.getTime() - date.getTimezoneOffset() * 60000)
|
* Get or create a cached DateTimeFormat instance
|
||||||
.toISOString()
|
*/
|
||||||
.slice(0, -1)
|
function getDateTimeFormatter(
|
||||||
.substring(0, 19);
|
options: Intl.DateTimeFormatOptions
|
||||||
|
): Intl.DateTimeFormat {
|
||||||
|
const key = JSON.stringify(options);
|
||||||
|
if (!formatterCache.has(key)) {
|
||||||
|
formatterCache.set(
|
||||||
|
key,
|
||||||
|
new Intl.DateTimeFormat([...window.navigator.languages], options)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return formatterCache.get(key)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create a cached RelativeTimeFormat instance
|
||||||
|
*/
|
||||||
|
function getRelativeTimeFormatter(locale: string): Intl.RelativeTimeFormat {
|
||||||
|
if (!rtfCache.has(locale)) {
|
||||||
|
rtfCache.set(locale, new Intl.RelativeTimeFormat(locale, { numeric: 'auto' }));
|
||||||
|
}
|
||||||
|
return rtfCache.get(locale)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date as relative time (e.g., "2 hours ago", "in 3 days")
|
||||||
|
*/
|
||||||
|
function formatTimeAgo(locale: string, date: Date): string {
|
||||||
|
const now = Date.now();
|
||||||
|
const targetTime = date.getTime();
|
||||||
|
let duration = (targetTime - now) / 1000;
|
||||||
|
|
||||||
|
const rtf = getRelativeTimeFormatter(locale);
|
||||||
|
|
||||||
|
// Use for...of for better performance and readability
|
||||||
|
for (const division of TIME_DIVISIONS) {
|
||||||
|
if (Math.abs(duration) < division.amount) {
|
||||||
|
return rtf.format(Math.round(duration), division.name);
|
||||||
|
}
|
||||||
|
duration /= division.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtf.format(0, 'seconds');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date-time string to locale-specific format
|
||||||
|
*/
|
||||||
|
export const formatDateTime = (dateTime: string): string => {
|
||||||
|
if (!dateTime || typeof dateTime !== 'string') {
|
||||||
|
return 'Invalid date';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract only the first 19 characters (YYYY-MM-DDTHH:mm:ss)
|
||||||
|
const cleanDateTime = dateTime.substring(0, 19);
|
||||||
|
const date = new Date(cleanDateTime);
|
||||||
|
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return 'Invalid date';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatter = getDateTimeFormatter({
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
|
||||||
|
return formatter.format(date);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error formatting date:', error);
|
||||||
|
return 'Invalid date';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a Date object to local date-time string (ISO format without timezone)
|
||||||
|
*/
|
||||||
|
export const formatLocalDateTime = (date: Date): string => {
|
||||||
|
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||||||
|
return 'Invalid date';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate local time offset in milliseconds
|
||||||
|
const offsetMs = date.getTimezoneOffset() * 60000;
|
||||||
|
const localTime = date.getTime() - offsetMs;
|
||||||
|
|
||||||
|
// Convert to ISO string and remove timezone info
|
||||||
|
return new Date(localTime).toISOString().slice(0, 19);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date with both short date format and relative time
|
||||||
|
*/
|
||||||
|
export const prettyDateTime = (locale: string, date: Date): string => {
|
||||||
|
if (!(date instanceof Date) || isNaN(date.getTime())) {
|
||||||
|
return 'Invalid date';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!locale || typeof locale !== 'string') {
|
||||||
|
locale = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortFormatter = getDateTimeFormatter({
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
|
||||||
|
const shortDate = shortFormatter.format(date);
|
||||||
|
const relativeTime = formatTimeAgo(locale, date);
|
||||||
|
|
||||||
|
return `${shortDate} (${relativeTime})`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -19,18 +19,18 @@
|
|||||||
my_build_flags =
|
my_build_flags =
|
||||||
|
|
||||||
[platformio]
|
[platformio]
|
||||||
; default_envs = s_16M_P ; BBQKees E32V2
|
default_envs = s_16M_P ; BBQKees E32V2
|
||||||
|
; default_envs = build_webUI ; build the web interface only
|
||||||
; default_envs = s3_16M_P ; BBQKees S3
|
; default_envs = s3_16M_P ; BBQKees S3
|
||||||
; default_envs = s_4M ; BBQKees older S32, 4MB no psram
|
; default_envs = s_4M ; BBQKees older S32, 4MB no psram
|
||||||
; default_envs = s_16M ; BBQKees newer S32 V2, 16MB no psram
|
; default_envs = s_16M ; BBQKees newer S32 V2, 16MB no psram
|
||||||
; default_envs = c6 ; XIAO ESP32C
|
; default_envs = c6 ; XIAO ESP32C
|
||||||
default_envs = debug
|
; default_envs = debug
|
||||||
|
|
||||||
[env]
|
[env]
|
||||||
|
|
||||||
; uncomment if you want to upload the firmware via OTA (must have upload_protocol = custom)
|
|
||||||
extra_scripts =
|
extra_scripts =
|
||||||
pre:scripts/build_interface.py ; builds the WebUI - comment out if you don't want to build each time
|
pre:scripts/build_interface.py ; builds the WebUI
|
||||||
scripts/rename_fw.py ; renames the firmware .bin file - comment out if not needed
|
scripts/rename_fw.py ; renames the firmware .bin file - comment out if not needed
|
||||||
scripts/upload.py ; optionally upload the firmware via OTA (must have upload_protocol = custom)
|
scripts/upload.py ; optionally upload the firmware via OTA (must have upload_protocol = custom)
|
||||||
|
|
||||||
@@ -46,28 +46,6 @@ custom_password = admin
|
|||||||
; upload_protocol = custom
|
; upload_protocol = custom
|
||||||
; custom_emsesp_ip = <ip address> or ems-esp.local
|
; custom_emsesp_ip = <ip address> or ems-esp.local
|
||||||
|
|
||||||
; upload_protocol = custom
|
|
||||||
; custom_emsesp_ip = 10.10.10.93 ; S3
|
|
||||||
custom_emsesp_ip = 192.168.1.223 ; E32V2
|
|
||||||
; custom_emsesp_ip = 192.168.1.173 ; S32
|
|
||||||
; custom_emsesp_ip = 192.168.1.59 ; S32 (old) 4MB blue board
|
|
||||||
|
|
||||||
; lib_deps =
|
|
||||||
; bblanchon/ArduinoJson @ 7.4.2
|
|
||||||
; ESP32Async/AsyncTCP @ 3.4.9
|
|
||||||
; ESP32Async/ESPAsyncWebServer @ 3.8.1
|
|
||||||
; ; file://${PROJECT_DIR}/../modules/EMS-ESP-Modules
|
|
||||||
; https://github.com/emsesp/EMS-ESP-Modules.git @ 1.0.8
|
|
||||||
|
|
||||||
; [espressif32_base_16M]
|
|
||||||
; framework = arduino
|
|
||||||
; board_build.partitions = partitions/esp32_partition_16M.csv
|
|
||||||
; board_upload.flash_size = 16MB
|
|
||||||
; board_build.app_partition_name = app0
|
|
||||||
; ; platform = espressif32@6.12.0 ; Arduino Core v2.0.17 / IDF v4.4.7
|
|
||||||
; ; platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20/platform-espressif32.zip ; Arduino Core v3.2.0 / ESP-IDF v5.4.1
|
|
||||||
; platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30/platform-espressif32.zip ; Arduino Core 3.3.0, IDF 5.5.0
|
|
||||||
|
|
||||||
; ** debug settings **
|
; ** debug settings **
|
||||||
; to be used with an ESP32-S3 which has an onboard JTAG, connect the OTG USB port to the PC
|
; to be used with an ESP32-S3 which has an onboard JTAG, connect the OTG USB port to the PC
|
||||||
; if using an external JTAG board like the ESP-PROG set debug_tool = esp-prog, and use zadig to set the correct driver for the USB port
|
; if using an external JTAG board like the ESP-PROG set debug_tool = esp-prog, and use zadig to set the correct driver for the USB port
|
||||||
|
|||||||
@@ -98,9 +98,7 @@ build_flags =
|
|||||||
build_unflags =
|
build_unflags =
|
||||||
${common.unbuild_flags}
|
${common.unbuild_flags}
|
||||||
extra_scripts =
|
extra_scripts =
|
||||||
; pre:scripts/build_interface.py ; builds the WebUI
|
post:scripts/rename_fw.py ; renames the firmware .bin file
|
||||||
scripts/rename_fw.py ; renames the firmware .bin file
|
|
||||||
; scripts/upload.py ; optionally upload the firmware via OTA (if upload_protocol = custom)
|
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
monitor_filters = direct
|
monitor_filters = direct
|
||||||
build_type = release
|
build_type = release
|
||||||
@@ -181,29 +179,14 @@ build_flags =
|
|||||||
${common.build_flags}
|
${common.build_flags}
|
||||||
-DBOARD_C6
|
-DBOARD_C6
|
||||||
|
|
||||||
;
|
; foundation for building and testing natively, standalone without an ESP32.
|
||||||
; Building and testing natively, standalone without an ESP32.
|
|
||||||
; See https://docs.platformio.org/en/latest/platforms/native.html
|
|
||||||
;
|
|
||||||
; It will generate an executable which when run will show the EMS-ESP Console where you can run tests using the `test` command.
|
|
||||||
;
|
|
||||||
; See https://docs.platformio.org/en/latest/core/installation/shell-commands.html#piocore-install-shell-commands
|
|
||||||
;
|
|
||||||
; to build and run directly on linux: pio run -e native -t exec
|
|
||||||
;
|
|
||||||
; to build and run on Windows, it needs winsock for the console input so:
|
|
||||||
; - For the first time, install Msys2 (https://www.msys2.org/) and the GCC compiler with `run pacman -S mingw-w64-ucrt-x86_64-gcc`
|
|
||||||
; - Then, build with `pio run -e native` to create the program.exe file
|
|
||||||
; - run by calling the executable from the Mysys shell e.g. `C:/msys64/msys2_shell.cmd -defterm -here -no-start -ucrt64 -c <location>/.pio/build/native/program.exe`
|
|
||||||
; - or use with Windows Terminal https://www.msys2.org/docs/terminals/
|
|
||||||
;
|
|
||||||
[env:native]
|
[env:native]
|
||||||
platform = native
|
platform = native
|
||||||
build_type = debug
|
build_type = debug
|
||||||
|
build_flags =
|
||||||
build_src_flags =
|
build_src_flags =
|
||||||
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
|
|
||||||
-DEMSESP_STANDALONE -DEMSESP_TEST
|
-DEMSESP_STANDALONE -DEMSESP_TEST
|
||||||
-DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev.0\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
-DARDUINOJSON_ENABLE_ARDUINO_STRING=1
|
||||||
-std=gnu++17 -Og -ggdb
|
-std=gnu++17 -Og -ggdb
|
||||||
-Wall -Wextra
|
-Wall -Wextra
|
||||||
-Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces
|
-Wno-unused-parameter -Wno-sign-compare -Wno-missing-braces
|
||||||
@@ -274,8 +257,45 @@ lib_deps = Unity
|
|||||||
test_testing_command =
|
test_testing_command =
|
||||||
${platformio.build_dir}/${this.__env__}/program
|
${platformio.build_dir}/${this.__env__}/program
|
||||||
|
|
||||||
|
;
|
||||||
|
; Building and testing locally on OS, which we call "standalone" without an ESP32.
|
||||||
|
; See https://docs.platformio.org/en/latest/platforms/native.html
|
||||||
|
;
|
||||||
|
; It will generate an executable which when run will show the EMS-ESP Console where you can run tests using the `test` command.
|
||||||
|
;
|
||||||
|
; See https://docs.platformio.org/en/latest/core/installation/shell-commands.html#piocore-install-shell-commands
|
||||||
|
;
|
||||||
|
; to build and run directly on linux: pio run -e standalone -t exec
|
||||||
|
;
|
||||||
|
; to build and run on Windows, it needs winsock for the console input so:
|
||||||
|
; - For the first time, install Msys2 (https://www.msys2.org/) and the GCC compiler with `run pacman -S mingw-w64-ucrt-x86_64-gcc`
|
||||||
|
; - Then, build with `pio run -e standalone` to create the program.exe file
|
||||||
|
; - run by calling the executable from the Mysys shell e.g. `C:/msys64/msys2_shell.cmd -defterm -here -no-start -ucrt64 -c <location>/.pio/build/native/program.exe`
|
||||||
|
; - or use with Windows Terminal https://www.msys2.org/docs/terminals/
|
||||||
|
;
|
||||||
|
[env:standalone]
|
||||||
|
extends = env:native
|
||||||
|
build_flags =
|
||||||
|
-DEMSESP_DEFAULT_LOCALE=\"en\" -DEMSESP_DEFAULT_TX_MODE=8 -DEMSESP_DEFAULT_VERSION=\"3.7.3-dev.0\" -DEMSESP_DEFAULT_BOARD_PROFILE=\"S32\"
|
||||||
|
|
||||||
|
; Modbus
|
||||||
|
; Creating the modbus registers is a multi-step process. Before it was in a shell script called generate_csv_and_headers.sh
|
||||||
|
; but now moved to pio so everything is in python and cross-platform. The logic is as follows:
|
||||||
|
; 1. create a dummy modbus_entity_parameters.hpp file so the first pass compiles
|
||||||
|
; 2. compile the EMS-ESPcode with the EMSESP_MODBUS flag set
|
||||||
|
; 3. run the entity_dump test command and generate the dump_entities.csv file
|
||||||
|
; 4. use the dump_entities.csv file with update_modbus_registers.py script to generate the modbus_entity_parameters.hpp file
|
||||||
|
; 5. clean up everything and start again with the EMSESP_STANDALONE flag set
|
||||||
|
; 6. run the entity_dump test command again to create the real dump_entities.csv file
|
||||||
|
; 7. create the Modbus-Entity-Registers.md file
|
||||||
|
; 8. create the dump_telegrams.csv file
|
||||||
|
;
|
||||||
|
; To run this in pio use the steps
|
||||||
|
; pio run -e build_modbus
|
||||||
|
; pio run -e build_standalone -t clean -t build
|
||||||
|
|
||||||
# builds the modbus_entity_parameters.hpp header file
|
# builds the modbus_entity_parameters.hpp header file
|
||||||
# pio run -e build_modbus -t build
|
# pio run -e build_modbus
|
||||||
[env:build_modbus]
|
[env:build_modbus]
|
||||||
extends = env:native
|
extends = env:native
|
||||||
targets = build
|
targets = build
|
||||||
@@ -287,8 +307,7 @@ custom_test_command = entity_dump
|
|||||||
custom_output_file = dump_entities.csv
|
custom_output_file = dump_entities.csv
|
||||||
custom_post_script = scripts/build_modbus_entity_parameters_post.py
|
custom_post_script = scripts/build_modbus_entity_parameters_post.py
|
||||||
|
|
||||||
; builds the real dump_entities.csv and dump_telegrams.csv files
|
; builds the real dump_entities.csv and dump_telegrams.csv files, and also the Modbus-Entity-Registers.md file
|
||||||
; and the Modbus-Entity-Registers.md file
|
|
||||||
; to be run after build_modbus with: pio run -e build_standalone -t clean -t build
|
; to be run after build_modbus with: pio run -e build_standalone -t clean -t build
|
||||||
[env:build_standalone]
|
[env:build_standalone]
|
||||||
extends = env:native
|
extends = env:native
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ def build_webUI(*args, **kwargs):
|
|||||||
env.Exit(1)
|
env.Exit(1)
|
||||||
env.Exit(0)
|
env.Exit(0)
|
||||||
|
|
||||||
# Create custom target that only runs the script
|
# Create custom target that only runs the script and then exits, without continuing with the pio workflow
|
||||||
env.AddCustomTarget(
|
env.AddCustomTarget(
|
||||||
name="build",
|
name="build",
|
||||||
dependencies=None,
|
dependencies=None,
|
||||||
|
|||||||
64
scripts/generate_csv_and_headers.sh
Normal file
64
scripts/generate_csv_and_headers.sh
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Builds the dump_*.csv files, modbus headers and modbus documentation.
|
||||||
|
# Run as `sh scripts/generate_csv_and_headers.sh` from the root of the repository.
|
||||||
|
|
||||||
|
##
|
||||||
|
## IMPORTANT NOTE!
|
||||||
|
## This script is not used anymore. It is kept for reference only.
|
||||||
|
## It has been replaced with two pio targets: build_modbus and build_standalone.
|
||||||
|
##
|
||||||
|
|
||||||
|
# create a dummy modbus_entity_parameters.hpp so the first pass compiles
|
||||||
|
cat >./src/core/modbus_entity_parameters.hpp <<EOL
|
||||||
|
#include "modbus.h"
|
||||||
|
#include "emsdevice.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is auto-generated. Do not modify.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// clang-format off
|
||||||
|
|
||||||
|
namespace emsesp {
|
||||||
|
|
||||||
|
using dt = EMSdevice::DeviceType;
|
||||||
|
|
||||||
|
#define REGISTER_MAPPING(device_type, device_value_tag_type, long_name, modbus_register_offset, modbus_register_count) \\
|
||||||
|
{ device_type, device_value_tag_type, long_name[0], modbus_register_offset, modbus_register_count }
|
||||||
|
|
||||||
|
// IMPORTANT: This list MUST be ordered by keys "device_type", "device_value_tag_type" and "modbus_register_offset" in this order.
|
||||||
|
const std::initializer_list<Modbus::EntityModbusInfo> Modbus::modbus_register_mappings = {};
|
||||||
|
|
||||||
|
} // namespace emsesp
|
||||||
|
|
||||||
|
// clang-format on
|
||||||
|
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# First generate Modbus entity parameters
|
||||||
|
# build the modbus_entity_parameters.hpp header file
|
||||||
|
make clean
|
||||||
|
make -s ARGS=-DEMSESP_MODBUS
|
||||||
|
rm -f ./src/core/modbus_entity_parameters.hpp ./docs/dump_entities.csv
|
||||||
|
echo "test entity_dump" | ./emsesp | python3 ./scripts/strip_csv.py > ./docs/dump_entities.csv
|
||||||
|
cat ./docs/dump_entities.csv | python3 ./scripts/update_modbus_registers.py > ./src/core/modbus_entity_parameters.hpp
|
||||||
|
|
||||||
|
# regenerate dump_entities.csv but without the Modbus entity parameters
|
||||||
|
make clean
|
||||||
|
make -s ARGS=-DEMSESP_STANDALONE
|
||||||
|
rm -f ./docs/dump_entities.csv
|
||||||
|
echo "test entity_dump" | ./emsesp | python3 ./scripts/strip_csv.py > ./docs/dump_entities.csv
|
||||||
|
|
||||||
|
# generate Modbus doc - Modbus-Entity-Registers.md used in the emsesp.org documentation
|
||||||
|
rm -f ./docs/Modbus-Entity-Registers.md
|
||||||
|
cat ./docs/dump_entities.csv | python3 ./scripts/generate-modbus-register-doc.py > ./docs/Modbus-Entity-Registers.md
|
||||||
|
|
||||||
|
# dump_telegrams.csv
|
||||||
|
rm -f ./docs/dump_telegrams.csv
|
||||||
|
echo "test telegram_dump" | ./emsesp | python3 ./scripts/strip_csv.py > ./docs/dump_telegrams.csv
|
||||||
|
|
||||||
|
ls -al ./src/core/modbus_entity_parameters.hpp
|
||||||
|
ls -al ./docs/Modbus-Entity-Registers.md
|
||||||
|
ls -al ./docs/dump_entities.csv
|
||||||
|
ls -al ./docs/dump_telegrams.csv
|
||||||
Reference in New Issue
Block a user