Merge branch 'dev_environment' of gitlab.com:brandx/bxt-app into dev_environment

This commit is contained in:
Carter 2025-04-16 09:33:53 -06:00
commit cb952b284e
17 changed files with 2300 additions and 1599 deletions

View file

@ -75,7 +75,7 @@ const useStyles = makeStyles((theme: Theme) => {
strokeOpacity: 0.85
},
grainInventory: {
fill: theme.palette.secondary.main,
fill: GRAIN_COLOUR,
opacity: 0.5
},
cableLine: {

View file

@ -0,0 +1,125 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
MenuItem,
TextField
} from "@mui/material";
import { useFirmwareAPI, usePrevious, useSnackbar } from "hooks";
import { Firmware } from "models";
import { pond } from "protobuf-ts/pond";
import { useEffect, useState } from "react";
interface Props {
firmware: Firmware;
isOpen: boolean;
closeCallback: Function;
refreshCallback: Function;
}
export default function UpdateFirmwareChannel(props: Props) {
const firmwareAPI = useFirmwareAPI();
const { isOpen, closeCallback, refreshCallback } = props;
const prevFirmware = usePrevious(props.firmware);
const { success, error } = useSnackbar();
const [firmware, setFirmware] = useState<Firmware>(Firmware.create());
useEffect(() => {
if (prevFirmware !== props.firmware) {
setFirmware(Firmware.clone(props.firmware));
}
}, [firmware, prevFirmware, props.firmware]);
const closeDialog = () => {
closeCallback();
};
const changeChannel = (event: any) => {
let updatedFirmware = firmware;
updatedFirmware.settings.channel = event.target.value;
setFirmware(updatedFirmware);
};
const update = () => {
firmwareAPI
.updateChannel(
firmware.settings.platform,
firmware.settings.version,
firmware.settings.channel
)
.then((response: any) => {
success("Firmware channel updated");
closeDialog();
refreshCallback();
})
.catch((err: any) => {
error(err);
});
};
if (firmware === null || firmware === undefined) {
return null;
} else {
return (
<Dialog fullWidth open={isOpen} onClose={closeDialog} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Upload Firmware</DialogTitle>
<DialogContent>
<TextField
disabled
id="platform"
name="platform"
select
label="Platform"
margin="normal"
fullWidth
variant="outlined"
value={firmware.settings.platform}>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_PHOTON}>Photon (Wifi)</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON}>
Electron (Cellular)
</MenuItem>
</TextField>
<TextField
disabled
id="version"
name="version"
autoFocus
label="Version"
margin="normal"
type="text"
fullWidth
variant="outlined"
value={firmware.settings.version}
/>
<TextField
id="channel"
name="channel"
select
required
label="Channel"
margin="normal"
fullWidth
variant="outlined"
value={firmware.settings.channel}
onChange={changeChannel}>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_RECOVERY}>Recovery</MenuItem>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT}>Development</MenuItem>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_ALPHA}>Alpha</MenuItem>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_BETA}>Beta</MenuItem>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_STABLE}>Stable</MenuItem>
</TextField>
</DialogContent>
<DialogActions>
<Button onClick={closeDialog} color="primary">
Cancel
</Button>
<Button onClick={update} color="primary">
Update
</Button>
</DialogActions>
</Dialog>
);
}
}

View file

@ -0,0 +1,304 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControlLabel,
IconButton,
InputBase,
MenuItem,
Paper,
Switch,
TextField,
Theme
} from "@mui/material";
import { createStyles } from "@mui/styles";
import withStyles, { WithStyles } from "@mui/styles/withStyles";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import { pond } from "protobuf-ts/pond";
import { FirmwareAPIContext } from "providers/pond/firmwareAPI";
import React from "react";
import semver from "semver";
import { or } from "utils/types";
const styles = (theme: Theme) => {
return createStyles({
root: {
padding: "2px 4px",
display: "flex",
alignItems: "center"
},
input: {
marginLeft: 8,
flex: 1
},
iconButton: {
padding: 10
}
});
};
interface Props extends WithStyles<typeof styles> {
isOpen: boolean;
closeDialogCallback: () => void;
refreshCallback: () => void;
}
interface State {
dirty: boolean;
version: string;
platform: pond.DevicePlatform;
channel: pond.UpgradeChannel;
file?: Blob;
breaksStorage: boolean;
}
class UploadFirmware extends React.Component<Props, State> {
static contextType = FirmwareAPIContext;
declare context: React.ContextType<typeof FirmwareAPIContext>;
selector: any;
constructor(props: Props) {
super(props);
this.state = {
dirty: false,
version: "",
platform: pond.DevicePlatform.DEVICE_PLATFORM_PHOTON,
channel: pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT,
breaksStorage: false
};
}
isValid() {
const { version, platform, channel, file } = this.state;
var isPlatformValid = [
pond.DevicePlatform.DEVICE_PLATFORM_PHOTON,
pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON,
pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR,
pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3,
pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK,
pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN,
pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE,
pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE
].includes(platform);
var isChannelValid = [
pond.UpgradeChannel.UPGRADE_CHANNEL_ALPHA,
pond.UpgradeChannel.UPGRADE_CHANNEL_BETA,
pond.UpgradeChannel.UPGRADE_CHANNEL_STABLE,
pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT
].includes(channel);
var isVersionValid = semver.valid(version) !== null;
var isFileValid = file !== undefined;
return isPlatformValid && isChannelValid && isVersionValid && isFileValid;
}
componentDidMount() {
this.selector = document.createElement("input");
this.selector.setAttribute("type", "file");
var self = this;
this.selector.addEventListener("change", function() {
if (self.selector.files && self.selector.files.length > 0) {
self.selectFile(self.selector.files[0]);
}
});
}
closeDialog = () => {
this.props.closeDialogCallback();
};
openFileSelector = () => {
this.selector.click();
};
prefill = (filename: string) => {
filename = filename.toLowerCase();
var state: State = {} as State;
if (filename.includes("photon") || filename.includes("wifi")) {
state.platform = pond.DevicePlatform.DEVICE_PLATFORM_PHOTON;
}
if (filename.includes("electron") || filename.includes("cell")) {
state.platform = pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON;
}
if (filename.includes("alpha")) {
state.channel = pond.UpgradeChannel.UPGRADE_CHANNEL_ALPHA;
}
if (filename.includes("beta")) {
state.channel = pond.UpgradeChannel.UPGRADE_CHANNEL_BETA;
}
if (filename.includes("stable")) {
state.channel = pond.UpgradeChannel.UPGRADE_CHANNEL_STABLE;
}
if (filename.includes("dev")) {
state.channel = pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT;
}
var version = filename.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)*/g);
if (version && version.length > 0) {
state.version = version[0];
}
if (filename.includes("break")) {
state.breaksStorage = true;
}
this.setState(state);
};
selectFile = (file: any) => {
const { dirty } = this.state;
if (!dirty) {
this.prefill(file.name);
}
this.setState({ file: file });
};
uploadFirmware = () => {
const { refreshCallback } = this.props;
const { version, channel, platform, file, breaksStorage } = this.state;
const firmwareAPI = this.context;
if (file) {
firmwareAPI
.uploadFirmware(version, channel, platform, file, breaksStorage)
.then(() => {
this.closeDialog();
refreshCallback();
})
.catch((err: any) => {
console.error(err ? err.message : "Error occurred while uploading firmware");
});
} else {
console.error("Cannot upload firmware, file not found");
}
};
render() {
const { classes, isOpen } = this.props;
const { version, channel, platform, file } = this.state;
return (
<Dialog
fullWidth
open={isOpen}
onClose={this.closeDialog}
aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Upload Firmware</DialogTitle>
<DialogContent>
<Paper className={classes.root} elevation={0}>
<IconButton
onClick={this.openFileSelector}
className={classes.iconButton}
aria-label="Select">
<CloudUploadIcon />
</IconButton>
<InputBase
disabled
className={classes.input}
placeholder="Select a binary"
value={or(file as any, { name: "" }).name}
/>
</Paper>
<TextField
id="platform"
name="platform"
select
required
label="Platform"
margin="normal"
fullWidth
variant="outlined"
value={platform}
onChange={event =>
this.setState({
platform: parseInt(event.target.value) as pond.DevicePlatform,
dirty: true
})
}>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_PHOTON}>Photon (Wifi)</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_ELECTRON}>
Electron (Cellular)
</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR}>V2 Cellular</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3}>V2 Wifi S3</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK}>
V2 Cellular Black
</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN}>
V2 Cellular Green
</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE}>
V2 Cellular Blue
</MenuItem>
<MenuItem value={pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE}>
V2 Wifi Blue
</MenuItem>
</TextField>
<TextField
id="channel"
name="channel"
select
required
label="Channel"
margin="normal"
fullWidth
variant="outlined"
value={channel}
onChange={event => {
this.setState({
channel: parseInt(event.target.value) as pond.UpgradeChannel,
dirty: true
});
}}>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_RECOVERY}>Recovery</MenuItem>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT}>Development</MenuItem>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_ALPHA}>Alpha</MenuItem>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_BETA}>Beta</MenuItem>
<MenuItem value={pond.UpgradeChannel.UPGRADE_CHANNEL_STABLE}>Stable</MenuItem>
</TextField>
<TextField
id="version"
name="version"
autoFocus
required
label="Version"
margin="normal"
type="text"
fullWidth
variant="outlined"
value={version}
onChange={event => {
this.setState({
version: event.target.value,
dirty: true
});
}}
/>
<FormControlLabel
control={
<Switch
name="breaksStorage"
checked={this.state.breaksStorage}
onChange={event => {
this.setState({
breaksStorage: event.target.checked,
dirty: true
});
}}
color="primary"
/>
}
label="Breaks Storage"
/>
</DialogContent>
<DialogActions>
<Button onClick={this.closeDialog} color="primary">
Cancel
</Button>
<Button onClick={this.uploadFirmware} color="primary" disabled={!this.isValid()}>
Upload
</Button>
</DialogActions>
</Dialog>
);
}
}
export default withStyles(styles)(UploadFirmware);

View file

@ -7,7 +7,7 @@ export {
// useComponentWebsocket,
useDeviceAPI,
// useDeviceWebsocket,
// useFirmwareAPI,
useFirmwareAPI,
// useGitlab,
useGroupAPI,
useHTTP,

View file

@ -330,7 +330,7 @@ import { Result } from "@mapbox/mapbox-gl-geocoder";
closeDrawers();
setBagDrawer(true);
setIgnoreFeatures(true);
setObjectKey(b.key);
setObjectKey(b.key());
moveMap(
b.centerLocation().latitude,
b.centerLocation().longitude,

View file

@ -8,6 +8,12 @@ interface FeatureVersionByPlatform {
electron: string;
v2Wifi: string;
v2Cell: string;
v2WifiS3: string;
v2CellS3: string;
v2CellBlack: string;
v2CellGreen: string;
v2WifiBlue: string;
v2CellBlue: string;
}
const featureVersions: Map<string, FeatureVersionByPlatform> = new Map([
@ -17,7 +23,13 @@ const featureVersions: Map<string, FeatureVersionByPlatform> = new Map([
photon: "1.19.64",
electron: "1.19.64",
v2Cell: "2.0.44",
v2Wifi: "2.0.44"
v2Wifi: "2.0.44",
v2WifiS3: "2.0.44",
v2CellS3: "2.0.44",
v2CellBlack: "2.0.44",
v2CellGreen: "2.0.44",
v2WifiBlue: "2.0.44",
v2CellBlue: "2.0.44"
}
]
]);
@ -94,6 +106,18 @@ export class Device {
return this.status.firmwareVersion >= versions.v2Cell;
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI:
return this.status.firmwareVersion >= versions.v2Wifi;
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3:
return this.status.firmwareVersion >= versions.v2WifiS3;
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_S3:
return this.status.firmwareVersion >= versions.v2CellS3;
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK:
return this.status.firmwareVersion >= versions.v2CellBlack;
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN:
return this.status.firmwareVersion >= versions.v2CellGreen;
case pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE:
return this.status.firmwareVersion >= versions.v2WifiBlue;
case pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE:
return this.status.firmwareVersion >= versions.v2CellBlue;
default:
return false;
}

View file

@ -38,6 +38,8 @@ const APIDocs = lazy(() => import("pages/APIDocs"));
const Fields = lazy(()=> import("pages/Fields"));
const Marketplace = lazy(() => import("userFeatures/UserFeatures"))
const Logs = lazy(() => import("pages/Logs"))
const Firmware = lazy(() => import("pages/Firmware"));
const Nfc = lazy(() => import("pages/Nfc"));
export const appendToUrl = (appendage: number | string) => {
const basePath = location.pathname.replace(/\/$/, "");
@ -311,6 +313,10 @@ export default function Router() {
}
<Route path="fields" element={<Fields />} />
<Route path="marketplace" element={<Marketplace />} />
{user.hasFeature("admin") && (
<Route path="firmware" element={<Firmware />} />
)}
<Route path="nfc" element={<Nfc />} />
{/* Map pages */}
<Route path="visualFarm" element={<FieldMap />} />

View file

@ -1,4 +1,4 @@
import { ChevronRight, Code, People, Person, SyncAlt } from "@mui/icons-material";
import { ChevronRight, Code, Memory, People, Person, SyncAlt, TapAndPlay } from "@mui/icons-material";
import ChevronLeft from "@mui/icons-material/ChevronLeft";
import {
darken,
@ -368,6 +368,20 @@ export default function SideNavigator(props: Props) {
</ListItemButton>
</Tooltip>
}
{user.hasFeature("admin") &&
<Tooltip title="Firmware" placement="right">
<ListItemButton
id="tour-firmware"
onClick={() => goTo("/firmware")}
classes={getClasses("/firmware")}
>
<ListItemIcon>
<Memory />
</ListItemIcon>
{open && <ListItemText primary="Logs" />}
</ListItemButton>
</Tooltip>
}
{user.hasFeature("admin") &&
<Tooltip title="Logs" placement="right">
<ListItemButton
@ -382,6 +396,20 @@ export default function SideNavigator(props: Props) {
</ListItemButton>
</Tooltip>
}
{user.hasFeature("admin") && window.NDEFReader &&
<Tooltip title="NFC" placement="right">
<ListItemButton
id="tour-nfc"
onClick={() => goTo("/nfc")}
classes={getClasses("/nfc")}
>
<ListItemIcon>
<TapAndPlay />
</ListItemIcon>
{open && <ListItemText primary="NFC" />}
</ListItemButton>
</Tooltip>
}
<Tooltip title="Marketplace" placement="right">
<ListItemButton
id="tour-marketplace"

View file

@ -0,0 +1,32 @@
import { Box, List, ListItem, ListItemIcon, ListItemText, Typography } from "@mui/material";
import { NfcCommand } from "pages/Nfc";
interface Props {
commands: NfcCommand[];
}
export default function CommandHistory(props: Props) {
const { commands } = props;
return (
<Box>
<Typography style={{ fontSize: 20, fontWeight: 650 }}>Session Command History</Typography>
<Box height={250} overflow="scroll" border={"2px solid white"} borderRadius={10}>
<List>
{commands.map((command, i) => (
<ListItem key={i}>
<ListItemIcon style={{ marginRight: 10 }}>
<Typography style={{ fontWeight: 650 }}>
{command.command + " : " + command.type}
</Typography>
</ListItemIcon>
<ListItemText>
<Typography>{command.data}</Typography>
</ListItemText>
</ListItem>
))}
</List>
</Box>
</Box>
);
}

109
src/nfc/nfcCommands.tsx Normal file
View file

@ -0,0 +1,109 @@
import { Box, Button, Grid, TextField } from "@mui/material";
import { NfcCommand } from "pages/Nfc";
import { useSnackbar } from "providers";
import { useState } from "react";
interface Props {
onCommandRun: (command: NfcCommand[]) => void;
}
export default function NfcCommands(props: Props) {
const { onCommandRun } = props;
const [textCommand, setTextCommand] = useState("");
const { openSnack } = useSnackbar();
const ndef = window.NDEFReader ? new NDEFReader() : undefined;
const writeCommand = (commandData: string) => {
if (ndef) {
ndef
.write({
records: [{ recordType: "text", data: commandData }]
})
.then(resp => {
openSnack("Comand sent");
onCommandRun([{ type: "text", command: "write", data: commandData }]);
})
.catch(error => {
openSnack("Failed to send command");
});
}
};
const readCommand = () => {
if (ndef) {
ndef.scan().then(() => {
//the scan started looking for the tag
openSnack("Starting Scan");
//the tag is found and can be read
ndef.onreading = event => {
let records: NfcCommand[] = [];
openSnack("Scan Complete");
//goes through the records found on the tag
event.message.records.forEach(record => {
//if the records type is set as tex
if (record.recordType === "text") {
//create a decoder using the records encoding method
let decoder = new TextDecoder(record.encoding);
//push the record type and the decoded data into the records to show in the command history
records.push({
type: record.recordType,
command: "read",
data: decoder.decode(record.data) //decode the record into a string to display
});
}
});
onCommandRun(records);
};
ndef.onreadingerror = event => {
openSnack("Failed to Complete Scan");
};
});
}
};
return (
<Box>
<TextField
label="NFC Command entry"
fullWidth
value={textCommand}
onChange={e => {
setTextCommand(e.target.value);
}}
style={{ paddingTop: 5, paddingBottom: 5 }}
/>
<Grid container spacing={2}>
<Grid item>
<Button
variant="contained"
color="primary"
onClick={() => {
if (ndef) {
writeCommand(textCommand);
} else {
openSnack("Browser does not support web nfc");
}
}}>
Write to NFC Tag
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
onClick={() => {
if (ndef) {
readCommand();
} else {
openSnack("Browser does not support web nfc");
}
}}>
Read NFC Tag
</Button>
</Grid>
</Grid>
</Box>
);
}

482
src/pages/Firmware.tsx Normal file
View file

@ -0,0 +1,482 @@
import {
Avatar,
Box,
IconButton,
ListItemAvatar,
ListItemText,
Menu,
MenuItem,
Theme,
Toolbar,
Tooltip,
TypeBackground
} from "@mui/material";
import { blue, green, orange, red } from "@mui/material/colors";
import { createStyles, makeStyles } from "@mui/styles";
//import { TypeBackground } from "@material-ui/core/styles/createPalette";
import {
ArrowUpward,
CloudDownload,
CloudUpload,
Delete,
MoreVert,
OpenInNew
} from "@mui/icons-material";
import classNames from "classnames";
import ResponsiveTable, { Column } from "common/ResponsiveTable";
import SmartBreadcrumb from "common/SmartBreadcrumb";
import UpdateFirmwareChannel from "firmware/UpdateFirmwareChannel";
import UploadFirmware from "firmware/UploadFirmware";
import { useFirmwareAPI, useSnackbar, useThemeType, useWidth } from "hooks";
//import MaterialTable, { Column, MTableToolbar } from "material-table";
import { Firmware as FirmwareModel } from "models";
import moment from "moment";
import PageContainer from "pages/PageContainer";
import { getDevicePlatformLabel, getDevicePlatformName } from "pbHelpers/DevicePlatform";
import { getUpgradeChannelColour, getUpgradeChannelLabel } from "pbHelpers/UpgradeChannel";
import { pond } from "protobuf-ts/pond";
import { useGlobalState } from "providers";
import { useCallback, useEffect, useState } from "react";
import { downloadFile } from "utils/download";
import { or } from "utils/types";
const useStyles = makeStyles((theme: Theme) => {
const themeType = useThemeType()
return ({
gutter: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
[theme.breakpoints.up("sm")]: {
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3)
}
},
actionsMenu: {
background:
themeType === "light"
? theme.palette.background["200" as keyof TypeBackground]
: theme.palette.background["700" as keyof TypeBackground]
},
avatar: {
margin: theme.spacing(1)
},
downloadIcon: {
color: theme.palette.text.primary,
backgroundColor: blue["500"],
"&:hover": {
backgroundColor: blue["600"]
}
},
updateIcon: {
color: theme.palette.text.primary,
backgroundColor: orange["500"],
"&:hover": {
backgroundColor: orange["600"]
}
},
changelogIcon: {
color: theme.palette.text.primary,
backgroundColor: green["500"],
"&:hover": {
backgroundColor: green["600"]
}
},
deleteIcon: {
color: theme.palette.text.primary,
backgroundColor: red["500"],
"&:hover": {
backgroundColor: red["600"]
}
},
menuItem: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
},
title: {
flex: "0 0 auto"
},
toolbar:
themeType === "light"
? {
position: "sticky",
justifyContent: "space-between",
background: "#fff",
zIndex: theme.zIndex.mobileStepper,
top: "55px",
paddingRight: theme.spacing(1),
[theme.breakpoints.up("sm")]: {
paddingRight: theme.spacing(2),
top: "63px"
}
}
: {
position: "sticky",
justifyContent: "space-between",
background: theme.palette.background["800" as keyof TypeBackground],
zIndex: theme.zIndex.mobileStepper,
top: "55px",
paddingRight: theme.spacing(1),
[theme.breakpoints.up("sm")]: {
paddingRight: theme.spacing(2),
top: "63px"
}
},
toolbarActions: {
color: theme.palette.text.secondary
},
cellPadding: {
paddingLeft: theme.spacing(0.5) + "px",
paddingRight: theme.spacing(0.5) + "px",
"@media (min-width: 321px)": {
paddingLeft: theme.spacing(1) + "px",
paddingRight: theme.spacing(1) + "px"
},
[theme.breakpoints.up("sm")]: {
paddingLeft: theme.spacing(4) + "px",
paddingRight: theme.spacing(4) + "px"
}
},
pointer: {
cursor: "pointer"
},
tableHead:
themeType === "light"
? {
position: "sticky",
background: "#fff",
zIndex: theme.zIndex.mobileStepper,
top: "110px",
[theme.breakpoints.up("sm")]: {
top: "126px"
}
}
: {
position: "sticky",
background: theme.palette.background["800" as keyof TypeBackground],
zIndex: theme.zIndex.mobileStepper,
top: "110px",
[theme.breakpoints.up("sm")]: {
top: "126px"
}
},
noFirmware: {
marginTop: theme.spacing(16)
},
darkIcon: {
color: theme.palette.primary.dark
},
loading: {
display: "flex",
alignItems: "center",
justifyContent: "center",
marginTop: theme.spacing(16)
}
})
})
export default function Firmware() {
const classes = useStyles();
const width = useWidth();
const { error, success } = useSnackbar();
const firmwareAPI = useFirmwareAPI();
const [{ user }] = useGlobalState();
const [pageSize, setPageSize] = useState<number>(5);
const [tablePage, setTablePage] = useState(0)
const [searchText, setSearchText] = useState("")
const [tableData, setTableData] = useState<FirmwareModel[]>([])
const [total, setTotal] = useState(0)
const [selected, setSelected] = useState<FirmwareModel | undefined>(undefined);
const [actionsOpen, setActionsOpen] = useState<boolean>(false);
const [isUploadFirmwareDialogOpen, setIsUploadFirmwareDialogOpen] = useState<boolean>(false);
const [isUpdateFirmwareChannelDialogOpen, setIsUpdateFirmwareChannelDialogOpen] = useState<
boolean
>(false);
const [anchor, setAnchor] = useState<any>(null);
// const TitleMap = new Map([
// ["Uploaded", "uploaded"],
// ["Version", "version"],
// ["Platform", "platform"],
// ["Channel", "channel"],
// ["Size", "size"]
// ]);
// let tableRef = useRef<any>(React.createRef());
// const reloadTable = () => {
// tableRef.current.onQueryChange();
// };
const load = useCallback(() => {
firmwareAPI.listFirmware(pageSize, pageSize * tablePage, "asc", "version", searchText)
.then((response) => {
setTotal(response.data.total ? response.data.total : 0)
let rows: FirmwareModel[] = or(response.data.firmware, []).map(
(firmwareData: any) => {
return FirmwareModel.create(pond.Firmware.fromObject(firmwareData));
}
);
setTableData(rows)
})
.catch(() => {
//failed to load
error("Failed to load firmware versions")
});
},[pageSize, tablePage, searchText])
useEffect(()=>{
load()
},[load])
const download = () => {
if (selected) {
firmwareAPI
.downloadInstaller(selected.settings.platform, selected.settings.version)
.then((response: Response) => {
var filename: string =
selected.settings.version +
"-" +
getDevicePlatformName(selected.settings.platform) +
".tar";
downloadFile(response, filename);
})
.catch((err: any) => {
error(
err.response ? err.response.statusText : "Error occured while downloading firmware"
);
})
.then(() => closeActionsMenu());
}
};
const hasChangelog = () => {
return (
selected && selected.settings.channel !== pond.UpgradeChannel.UPGRADE_CHANNEL_DEVELOPMENT
);
};
const viewChangelog = () => {
if (selected && selected.settings.version) {
window.open(
"https://gitlab.com/brandx/gadwall/-/tags/" + selected.settings.version,
"_blank"
);
}
};
const deleteFirmware = () => {
if (selected) {
firmwareAPI
.removeFirmware(selected.settings.platform, selected.settings.version)
.then((response: any) => {
success("Successfully removed firmware");
})
.catch((err: any) => {
error(
err.response
? err.response.statusText
: "Error occured while delete the firmware release"
);
})
.then(() => {
closeActionsMenu();
load();
});
}
};
const openActionsMenu = (firmware: FirmwareModel) => (event: any) => {
if (user.settings.actions.includes("upload-firmware")) {
setActionsOpen(true);
setAnchor(event.currentTarget);
setSelected(firmware);
} else {
error("Missing upload-firmware action");
}
};
const closeActionsMenu = () => {
setActionsOpen(false);
setAnchor(null);
setSelected(undefined);
};
const openUploadFirmwareDialog = (event: any) => {
if (user.settings.actions.includes("upload-firmware")) {
setActionsOpen(false);
setAnchor(event.currentTarget);
setIsUploadFirmwareDialogOpen(true);
} else {
error("Missing upload-firmware action");
}
};
const closeUploadFirmwareDialog = () => {
setActionsOpen(false);
setAnchor(undefined);
setSelected(undefined);
setIsUploadFirmwareDialogOpen(false);
};
const openUpdateFirmwareChannelDialog = (event: any) => {
setActionsOpen(false);
setAnchor(event.currentTarget);
setIsUpdateFirmwareChannelDialogOpen(true);
};
const closeUpdateFirmwareChannelDialog = () => {
setActionsOpen(false);
setAnchor(undefined);
setSelected(undefined);
setIsUpdateFirmwareChannelDialogOpen(false);
};
const renderActionsMenu = () => {
return (
<Menu
id="actionsMenu"
anchorEl={anchor}
open={actionsOpen}
onClose={closeActionsMenu}
disableAutoFocusItem
MenuListProps={{ classes: { root: classes.actionsMenu } }}>
<MenuItem className={classes.menuItem} onClick={download}>
<ListItemAvatar>
<Avatar
className={classNames(classes.avatar, classes.downloadIcon)}
alt="Cloud Download">
<CloudDownload className={classes.downloadIcon} />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Download Installer" />
</MenuItem>
<MenuItem className={classes.menuItem} onClick={openUpdateFirmwareChannelDialog}>
<ListItemAvatar>
<Avatar className={classNames(classes.avatar, classes.updateIcon)} alt="Update">
<ArrowUpward className={classes.updateIcon} />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Update Channel" />
</MenuItem>
<MenuItem
disabled={!hasChangelog()}
className={classes.menuItem}
onClick={viewChangelog}>
<ListItemAvatar>
<Avatar className={classNames(classes.avatar, classes.changelogIcon)} alt="Open New">
<OpenInNew className={classes.changelogIcon} />
</Avatar>
</ListItemAvatar>
<ListItemText primary="View Changelog" />
</MenuItem>
<MenuItem className={classes.menuItem} onClick={deleteFirmware}>
<ListItemAvatar>
<Avatar className={classNames(classes.avatar, classes.deleteIcon)} alt="Delete">
<Delete className={classes.deleteIcon} />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Delete Version" />
</MenuItem>
</Menu>
);
};
const columns = (small: boolean): Column<FirmwareModel>[] => {
return [
{
title: "Uploaded",
hidden: small,
//defaultSort: "desc",
cellStyle: {padding: 2},
render: row => {
return <span>{moment(row.status.uploaded).fromNow()}</span>;
}
},
{
title: "Version",
cellStyle: {padding: 2},
render: row => <span>{row.settings.version}</span>
},
{
title: "Platform",
cellStyle: {padding: 2},
render: row => <span>{getDevicePlatformLabel(row.settings.platform)}</span>
},
{
title: "Channel",
cellStyle: {padding: 2},
render: row => (
<span style={{ color: getUpgradeChannelColour(row.settings.channel) }}>
{getUpgradeChannelLabel(row.settings.channel)}
</span>
)
},
{
title: "Size",
cellStyle: {padding: 2},
//type: "numeric",
hidden: small,
render: row => <span>{row.settings.size} bytes</span>
},
{
title: "Actions",
cellStyle: {padding: 2},
//type: "numeric",
//sorting: false,
render: row => (
<IconButton aria-label="Actions" onClick={openActionsMenu(row)}>
<MoreVert fontSize={small ? "small" : "inherit"} />
</IconButton>
)
}
];
};
const actionToolbar = () => {
return (
<Toolbar className={classes.gutter}>
{user.allowedTo("upload-firmware") && (
<Tooltip title="Upload Firmware" placement="bottom">
<span>
<IconButton aria-label="Upload Firmware" onClick={openUploadFirmwareDialog}>
<CloudUpload
fontSize={width === "xs" ? "small" : "inherit"}
className={classes.darkIcon}
/>
</IconButton>
</span>
</Tooltip>
)}
</Toolbar>
);
};
return (
<PageContainer>
<Box margin={2}>
<ResponsiveTable<FirmwareModel>
subtitle={actionToolbar()}
setSearchText={(search) => setSearchText(search)}
title={<SmartBreadcrumb />}
page={tablePage}
pageSize={pageSize}
rows={tableData}
total={total}
columns={columns(width === "xs" || width === "sm")}
setPage={(page)=>{setTablePage(page)}}
handleRowsPerPageChange={(e)=>{setPageSize(e.target.value)}}
/>
</Box>
<UploadFirmware
isOpen={isUploadFirmwareDialogOpen}
closeDialogCallback={closeUploadFirmwareDialog}
refreshCallback={() => load()}
/>
<UpdateFirmwareChannel
firmware={selected ? selected : FirmwareModel.create()}
isOpen={isUpdateFirmwareChannelDialogOpen}
closeCallback={closeUpdateFirmwareChannelDialog}
refreshCallback={() => load()}
/>
{renderActionsMenu()}
</PageContainer>
);
}

34
src/pages/Nfc.tsx Normal file
View file

@ -0,0 +1,34 @@
import { Box } from "@mui/material";
import { cloneDeep } from "lodash";
import CommandHistory from "nfc/commandHistory";
import NfcCommands from "nfc/nfcCommands";
import React, { useState } from "react";
import PageContainer from "./PageContainer";
export interface NfcCommand {
type: string;
command: "read" | "write";
data: string;
}
export default function Nfc() {
const [executedCommands, setExecutedCommands] = useState<NfcCommand[]>([]);
return (
<PageContainer>
<React.Fragment>
<Box padding={2}>
<CommandHistory commands={executedCommands} />
<NfcCommands
onCommandRun={commands => {
//add to the list of executed commands
let c = cloneDeep(executedCommands);
c.push(...commands);
setExecutedCommands(c);
}}
/>
</Box>
</React.Fragment>
</PageContainer>
);
}

View file

@ -80,6 +80,24 @@ const V2_Cell_Green: DevicePlatformDescriber = {
platformName: "v2cellgreen"
};
const V2_Cell_Blue: DevicePlatformDescriber = {
platform: pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE,
label: "V2 Cellular Blue",
description: "Communicates via cellular",
icon: <SignalCellular4Bar style={{ color: "#fff" }} />,
offlineIcon: <SignalCellularOff style={{ color: "#fff" }} />,
platformName: "v2cellblue"
};
const V2_Wifi_Blue: DevicePlatformDescriber = {
platform: pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE,
label: "V2 Wifi Blue",
description: "Communicates via wifi",
icon: <SignalCellular4Bar style={{ color: "#fff" }} />,
offlineIcon: <SignalCellularOff style={{ color: "#fff" }} />,
platformName: "v2wifiblue"
};
const map = new Map<pond.DevicePlatform, DevicePlatformDescriber>([
[pond.DevicePlatform.DEVICE_PLATFORM_INVALID, Default],
[pond.DevicePlatform.DEVICE_PLATFORM_PHOTON, Photon],
@ -87,7 +105,9 @@ const map = new Map<pond.DevicePlatform, DevicePlatformDescriber>([
[pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR, V2_Cellular],
[pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_S3, V2_Wifi_S3],
[pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLACK, V2_Cell_Black],
[pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN, V2_Cell_Green]
[pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_GREEN, V2_Cell_Green],
[pond.DevicePlatform.DEVICE_PLATFORM_V2_CELLULAR_BLUE, V2_Cell_Blue],
[pond.DevicePlatform.DEVICE_PLATFORM_V2_WIFI_BLUE, V2_Wifi_Blue]
]);
export function getDevicePlatformDescriber(platform: pond.DevicePlatform): DevicePlatformDescriber {

View file

@ -149,14 +149,10 @@ export default function FirmwareProvider(props: PropsWithChildren<Props>) {
file: Blob,
breaksStorage: boolean
) => {
// let headers: { "Content-Type": string } = {
// "Content-Type": "multipart/form-data"
// };
let headers = {
"Content-Type": "multipart/form-data",
...options().headers
...options().headers,
"Content-Type": "multipart/form-data"
};
headers = { ...headers, ...options().headers };
let opt = { headers };
let data = new FormData();
data.append("version", version);
@ -164,6 +160,7 @@ export default function FirmwareProvider(props: PropsWithChildren<Props>) {
data.append("platform", platformBody(platform));
data.append("file", file);
data.append("breaksStorage", breaksStorage.toString());
console.log(data)
return post(pondURL("/firmware"), data, opt);
};

80
src/web-nfc.d.ts vendored Normal file
View file

@ -0,0 +1,80 @@
// Type definitions for Web NFC
// Project: https://github.com/w3c/web-nfc
// Definitions by: Takefumi Yoshii <https://github.com/takefumi-yoshii>
// TypeScript Version: 3.9
// This type definitions referenced to WebIDL.
// https://w3c.github.io/web-nfc/#actual-idl-index
interface Window {
NDEFMessage: NDEFMessage;
}
declare class NDEFMessage {
constructor(messageInit: NDEFMessageInit);
records: ReadonlyArray<NDEFRecord>;
}
declare interface NDEFMessageInit {
records: NDEFRecordInit[];
}
declare type NDEFRecordDataSource = string | BufferSource | NDEFMessageInit;
interface Window {
NDEFRecord: NDEFRecord;
}
declare class NDEFRecord {
constructor(recordInit: NDEFRecordInit);
readonly recordType: string;
readonly mediaType?: string;
readonly id?: string;
readonly data?: DataView;
readonly encoding?: string;
readonly lang?: string;
toRecords?: () => NDEFRecord[];
}
declare interface NDEFRecordInit {
recordType: string;
mediaType?: string;
id?: string;
encoding?: string;
lang?: string;
data?: NDEFRecordDataSource;
}
declare type NDEFMessageSource = string | BufferSource | NDEFMessageInit;
interface Window {
NDEFReader: NDEFReader;
}
declare class NDEFReader extends EventTarget {
constructor();
onreading: (this: this, event: NDEFReadingEvent) => any;
onreadingerror: (this: this, error: Event) => any;
scan: (options?: NDEFScanOptions) => Promise<void>;
write: (message: NDEFMessageSource, options?: NDEFWriteOptions) => Promise<void>;
makeReadOnly: (options?: NDEFMakeReadOnlyOptions) => Promise<void>;
}
interface Window {
NDEFReadingEvent: NDEFReadingEvent;
}
declare class NDEFReadingEvent extends Event {
constructor(type: string, readingEventInitDict: NDEFReadingEventInit);
serialNumber: string;
message: NDEFMessage;
}
interface NDEFReadingEventInit extends EventInit {
serialNumber?: string;
message: NDEFMessageInit;
}
interface NDEFWriteOptions {
overwrite?: boolean;
signal?: AbortSignal;
}
interface NDEFMakeReadOnlyOptions {
signal?: AbortSignal;
}
interface NDEFScanOptions {
signal: AbortSignal;
}