Initial Commit

This commit is contained in:
Patrick McDonagh
2018-04-03 18:09:54 -05:00
commit 6f7381c75d
27 changed files with 7825 additions and 0 deletions

6293
app/build/bundle.js Normal file

File diff suppressed because one or more lines are too long

56
app/build/main.css Normal file
View File

@@ -0,0 +1,56 @@
.react-vis-magic-css-import-rule{display:inherit}.rv-treemap{font-size:12px;position:relative}.rv-treemap__leaf{overflow:hidden;position:absolute}.rv-treemap__leaf--circle{align-items:center;border-radius:100%;display:flex;justify-content:center}.rv-treemap__leaf__content{overflow:hidden;padding:10px;text-overflow:ellipsis}.rv-xy-plot{color:#c3c3c3;position:relative}.rv-xy-plot canvas{pointer-events:none}.rv-xy-plot .rv-xy-canvas{pointer-events:none;position:absolute}.rv-xy-plot__inner{display:block}.rv-xy-plot__axis__line{fill:none;stroke-width:2px;stroke:#e6e6e9}.rv-xy-plot__axis__tick__line{stroke:#e6e6e9}.rv-xy-plot__axis__tick__text{fill:#6b6b76;font-size:11px}.rv-xy-plot__axis__title text{fill:#6b6b76;font-size:11px}.rv-xy-plot__grid-lines__line{stroke:#e6e6e9}.rv-xy-plot__circular-grid-lines__line{fill-opacity:0;stroke:#e6e6e9}.rv-xy-plot__circular-grid-lines__line{fill-opacity:0;stroke:#e6e6e9}.rv-xy-plot__series,.rv-xy-plot__series path{pointer-events:all}.rv-xy-plot__series--line{fill:none;stroke:#000;stroke-width:2px}.rv-crosshair{position:absolute;font-size:11px;pointer-events:none}.rv-crosshair__line{background:#47d3d9;width:1px}.rv-crosshair__inner{position:absolute;text-align:left;top:0}.rv-crosshair__inner__content{border-radius:4px;background:#3a3a48;color:#fff;font-size:12px;padding:7px 10px;box-shadow:0 2px 4px rgba(0,0,0,0.5)}.rv-crosshair__inner--left{right:4px}.rv-crosshair__inner--right{left:4px}.rv-crosshair__title{font-weight:bold;white-space:nowrap}.rv-crosshair__item{white-space:nowrap}.rv-hint{position:absolute;pointer-events:none}.rv-hint__content{border-radius:4px;padding:7px 10px;font-size:12px;background:#3a3a48;box-shadow:0 2px 4px rgba(0,0,0,0.5);color:#fff;text-align:left;white-space:nowrap}.rv-discrete-color-legend{box-sizing:border-box;overflow-y:auto;font-size:12px}.rv-discrete-color-legend.horizontal{white-space:nowrap}.rv-discrete-color-legend-item{color:#3a3a48;border-radius:1px;padding:9px 10px}.rv-discrete-color-legend-item.horizontal{display:inline-block}.rv-discrete-color-legend-item.horizontal .rv-discrete-color-legend-item__title{margin-left:0;display:block}.rv-discrete-color-legend-item__color{background:#dcdcdc;display:inline-block;height:2px;vertical-align:middle;width:14px}.rv-discrete-color-legend-item__title{margin-left:10px}.rv-discrete-color-legend-item.disabled{color:#b8b8b8}.rv-discrete-color-legend-item.clickable{cursor:pointer}.rv-discrete-color-legend-item.clickable:hover{background:#f9f9f9}.rv-search-wrapper{display:flex;flex-direction:column}.rv-search-wrapper__form{flex:0}.rv-search-wrapper__form__input{width:100%;color:#a6a6a5;border:1px solid #e5e5e4;padding:7px 10px;font-size:12px;box-sizing:border-box;border-radius:2px;margin:0 0 9px;outline:0}.rv-search-wrapper__contents{flex:1;overflow:auto}.rv-continuous-color-legend{font-size:12px}.rv-continuous-color-legend .rv-gradient{height:4px;border-radius:2px;margin-bottom:5px}.rv-continuous-size-legend{font-size:12px}.rv-continuous-size-legend .rv-bubbles{text-align:justify;overflow:hidden;margin-bottom:5px;width:100%}.rv-continuous-size-legend .rv-bubble{background:#d8d9dc;display:inline-block;vertical-align:bottom}.rv-continuous-size-legend .rv-spacer{display:inline-block;font-size:0;line-height:0;width:100%}.rv-legend-titles{height:16px;position:relative}.rv-legend-titles__left,.rv-legend-titles__right,.rv-legend-titles__center{position:absolute;white-space:nowrap;overflow:hidden}.rv-legend-titles__center{display:block;text-align:center;width:100%}.rv-legend-titles__right{right:0}.rv-radial-chart .rv-xy-plot__series--label{pointer-events:none}
/*
* Global constants goes here
*/
:root {
--primary-color: #42b983;
}
/*
* Global CSS goes here, it requires to use :global before each style
*/
html {
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
margin: auto;
}
#app {
color: #2c3e50;
font-family: Source Sans Pro, Helvetica, sans-serif;
height: 100%;
width:100%;
}
#app p {
text-align: justify;
}
.hello {
color: var(--primary-color);
}
#permissives-table tbody tr td {
text-align: center;
}
#permissives-table tbody tr td button {
width: 95%;
}
#permissives-table thead tr th {
text-align: center;
}
#settings-button {
margin: auto 15px;
}
.control-button {
width: 95%;
}

16
app/index.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Max Water System</title>
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.98.1/css/materialize.min.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="build/main.css">
</head>
<body>
<div id="app"></div>
<script src="build/bundle.js"></script>
</body>
</html>

View File

@@ -0,0 +1,30 @@
import { ipcRenderer } from "electron";
export const SET_PLC_IPADDRESS = "SET_PLC_IPADDRESS";
export const INITIALIZE_PLC = "INITIALIZE_PLC";
export const PLC_DATA_RECEIVED = "PLC_DATA_RECEIVED";
export function setPlcIpAddress(ipAddress){
return {
type: SET_PLC_IPADDRESS,
payload: ipAddress
};
}
export function ipcPlcInitializeSend(ipAddress, tagList){
ipcRenderer.send("plc:initialize", ipAddress, tagList);
return {
type: INITIALIZE_PLC,
payload: true
};
}
export function ipcPlcDetailsReceived(event, plcData){
console.log("action creator got PLC data", plcData);
return {
type: PLC_DATA_RECEIVED,
payload: plcData
};
}

View File

@@ -0,0 +1,46 @@
import { ipcRenderer } from "electron";
export const IPC_TAGUPDATE = "IPC_TAGUPDATE";
export const IPC_TAGSYNC = "IPC_TAGSYNC";
export const STORE_NEW_TAG = "STORE_NEW_TAG";
export const DELETE_TAG = "DELETE_TAG";
export const WRITE_TAG = "WRITE_TAG";
export function ipcTagUpdate(event, tag){
return {
type: IPC_TAGUPDATE,
payload: tag.state.tag
};
}
export function ipcTagSync(ipAddress, tagList){
ipcRenderer.send("tag:sync", ipAddress, tagList);
return {
type: IPC_TAGSYNC,
payload: true
};
}
export function storeNewTag(tag){
return {
type: STORE_NEW_TAG,
payload: tag
};
}
export function deleteTag(tagName){
return {
type: DELETE_TAG,
payload: tagName
};
}
export function writeTag(tagName, value){
ipcRenderer.send("tag:write", tagName, value);
return {
type: WRITE_TAG,
payload: true
};
}

BIN
app/src/assets/electron.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
app/src/assets/react.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
app/src/assets/webpack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@@ -0,0 +1,260 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { writeTag } from "../actions/actions_tags";
class Controls extends Component {
constructor(props){
super(props);
this.state = {
setpoints: {}
};
}
writeStart(){
this.props.writeTag("cmd_Start", true);
}
writeStop(){
this.props.writeTag("cmd_Stop", true);
}
onSetpointChange = (event, parName) => {
const { value } = event.target;
this.setState({setpoints: {...this.state.setpoints, [parName]: value}});
}
onSetpointSubmit = (event, parName) => {
event.preventDefault();
switch(parName){
case "flowrate":
this.props.writeTag("cfg_PID_FlowSP", this.state.setpoints.flowrate);
break;
case "fluidlevel":
this.props.writeTag("cfg_PID_FluidLevelSP", this.state.setpoints.fluidlevel);
break;
case "tubingpressure":
this.props.writeTag("cfg_PID_TubingPressureSP", this.state.setpoints.tubingpressure);
break;
case "frequency":
this.props.writeTag("cfg_PID_ManualSP", this.state.setpoints.frequency);
break;
}
}
onPIDParameterSelect = (e, parName) => {
event.preventDefault();
switch(parName){
case "flowrate":
this.props.writeTag("cfg_PID_Flow", true);
break;
case "fluidlevel":
this.props.writeTag("cfg_PID_FluidLevel", true);
break;
case "tubingpressure":
this.props.writeTag("cfg_PID_TubingPressure", true);
break;
case "frequency":
this.props.writeTag("cfg_PID_Manual", true);
break;
}
}
componentDidMount(){
if (this.props.tags){
this.setState({
setpoints: {
flowrate: this.props.tags.cfg_PID_FlowSP.value,
fluidlevel: this.props.tags.cfg_PID_FluidLevelSP.value,
tubingpressure: this.props.tags.cfg_PID_TubingPressureSP.value,
frequency: this.props.tags.cfg_PID_ManualSP.value,
}
});
}
}
render(){
if (!this.props.tags){
return(
<div>
<h1>Loading...</h1>
</div>
);
}
if (!this.props.tags.Device_Status_INT){
return(
<div>
<h1>Loading...</h1>
</div>
);
}
const startBtnClass = this.props.tags.Device_Status_INT.value === 4 ? "btn btn-success control-button" : "btn btn-secondary disabled control-button";
const stopBtnClass = this.props.tags.Device_Status_INT.value !== 4 ? "btn btn-danger control-button" : "btn btn-secondary disabled control-button";
return (
<div className="container" style={{textAlign: "center"}}>
<h1>Controls</h1>
<table className="table">
<tbody>
<tr>
<td>
<button
className={startBtnClass}
onClick={() => this.writeStart()}
>Start</button>
</td>
<td>
<button
className={stopBtnClass}
onClick={() => this.writeStop()}
>Stop</button>
</td>
</tr>
</tbody>
</table>
<hr />
<h3>Control Setpoints</h3>
<table className="table">
<tbody>
<tr>
<td>
<button
className={this.props.tags.sts_PID_Control.value === 0 ? "btn btn-success disabled" : "btn btn-light"}
onClick={(e) => this.onPIDParameterSelect(e, "flowrate")}
>
Flow Rate
</button>
</td>
<td>
<button
className={this.props.tags.sts_PID_Control.value === 1 ? "btn btn-success disabled" : "btn btn-light"}
onClick={(e) => this.onPIDParameterSelect(e, "fluidlevel")}
>
Fluid Level
</button>
</td>
<td>
<button
className={this.props.tags.sts_PID_Control.value === 2 ? "btn btn-success disabled" : "btn btn-light"}
onClick={(e) => this.onPIDParameterSelect(e, "tubingpressure")}
>
Tubing Pressure
</button>
</td>
<td>
<button
className={this.props.tags.sts_PID_Control.value === 3 ? "btn btn-success disabled" : "btn btn-light"}
onClick={(e) => this.onPIDParameterSelect(e, "frequency")}
>
Manual Frequency
</button>
</td>
</tr>
<tr>
<td>
<input
value={this.state.setpoints.flowrate}
onChange={(e) => this.onSetpointChange(e, "flowrate")}
className="form-control"
type="number"
/>
</td>
<td>
<input
value={this.state.setpoints.fluidlevel}
onChange={(e) => this.onSetpointChange(e, "fluidlevel")}
className="form-control"
type="number"
/>
</td>
<td>
<input
value={this.state.setpoints.tubingpressure}
onChange={(e) => this.onSetpointChange(e, "tubingpressure")}
className="form-control"
type="number"
/>
</td>
<td>
<input
value={this.state.setpoints.frequency}
onChange={(e) => this.onSetpointChange(e, "frequency")}
className="form-control"
type="number"
/>
</td>
</tr>
<tr>
<td>
<button
onClick={(e) => this.onSetpointSubmit(e, "flowrate")}
className="btn btn-primary"
>
Update
</button>
</td>
<td>
<button
onClick={(e) => this.onSetpointSubmit(e, "fluidlevel")}
className="btn btn-primary"
>
Update
</button>
</td>
<td>
<button
onClick={(e) => this.onSetpointSubmit(e, "tubingpressure")}
className="btn btn-primary"
>
Update
</button>
</td>
<td>
<button
onClick={(e) => this.onSetpointSubmit(e, "frequency")}
className="btn btn-primary"
>
Update
</button>
</td>
</tr>
</tbody>
</table>
</div>
);
}
}
function mapStateToProps(state){
return {
tags: state.tags
};
}
export default connect(mapStateToProps, { writeTag })(Controls);

View File

@@ -0,0 +1,76 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import FontAwesomeIcon from "@fortawesome/react-fontawesome";
import { faTint, faCog } from "@fortawesome/fontawesome-pro-regular";
class Header extends Component {
renderRunningState(){
if(!this.props.tags ){
return <span></span>;
}
if (!this.props.tags.Device_Status_INT){
return <span></span>;
}
let deviceStatus;
let deviceClass;
switch(this.props.tags.Device_Status_INT.value){
case 0:
deviceStatus = "Running";
deviceClass = "btn btn-success";
break;
case 1:
deviceStatus = "Pumped Off";
deviceClass = "btn btn-warning";
break;
case 2:
deviceStatus = "Alarmed";
deviceClass = "btn btn-danger";
break;
case 3:
deviceStatus = "Locked Out";
deviceClass = "btn btn-danger";
break;
case 4:
deviceStatus = "Stopped";
deviceClass = "btn btn-secondary";
break;
default:
deviceStatus = "Unknown";
deviceClass = "btn btn-info";
}
return <button className={deviceClass + " disabled"}>{deviceStatus}</button>;
}
render(){
return (
<nav className="navbar navbar-light navbar-expand-sm bg-light">
<Link to="/" className="navbar-brand">
<FontAwesomeIcon icon={faTint} /> Max Water System
</Link>
<div className="navbar-nav">
<Link className="nav-item nav-link" to="/controls">Control</Link>
<Link className="nav-item nav-link" to="/permissives">Permissives</Link>
<Link className="nav-item nav-link" to="/alltags">All Tags</Link>
</div>
<div className="navbar-nav ml-auto">
<span className="navbar-text">{this.renderRunningState()}</span>
<Link className="nav-item nav-link" to="/settings" id="settings-button"><FontAwesomeIcon icon={faCog} /></Link>
</div>
</nav>
);
}
}
function mapStateToProps(state){
return {
tags: state.tags
};
}
export default connect(mapStateToProps)(Header);

101
app/src/components/Main.js Normal file
View File

@@ -0,0 +1,101 @@
import React, { Component } from "react";
import _ from "lodash";
import { connect } from "react-redux";
import { FlexibleWidthXYPlot, LineSeries, VerticalGridLines, HorizontalGridLines, XAxis, YAxis, DiscreteColorLegend } from "react-vis";
import "../../../node_modules/react-vis/dist/style.css";
class Main extends Component {
getChartValues(values){
return _.map(values, (val) => {
return {x: val.timestamp, y: val.value};
});
}
render(){
if (!this.props.tagHistory){
return(
<div className="container">
<h1>Loading...</h1>
</div>
);
}
return (
<div className="container">
<h3>Process Values</h3>
<FlexibleWidthXYPlot
height={300}
xType="time"
>
<VerticalGridLines />
<HorizontalGridLines />
<XAxis />
<YAxis />
<LineSeries
data={this.getChartValues(this.props.tagHistory.val_IntakePressure)}
/>
<LineSeries
data={this.getChartValues(this.props.tagHistory.val_Flowmeter)}
/>
<LineSeries
data={this.getChartValues(this.props.tagHistory.val_FluidLevel)}
/>
</FlexibleWidthXYPlot>
<DiscreteColorLegend
items={[
{title: "Intake Pressure"},
{title: "Flowmeter"},
{title: "Fluid Level"}
]}
orientation="horizontal"
/>
<hr />
<h3>VFD Data</h3>
<FlexibleWidthXYPlot
height={300}
xType="time"
>
<VerticalGridLines />
<HorizontalGridLines />
<XAxis />
<YAxis />
<LineSeries
data={this.getChartValues(this.props.tagHistory.VFD_OutCurrent)}
/>
<LineSeries
data={this.getChartValues(this.props.tagHistory.VFD_SpeedFdbk)}
/>
<LineSeries
data={this.getChartValues(this.props.tagHistory.VFD_OutPower)}
/>
<LineSeries
data={this.getChartValues(this.props.tagHistory.VFD_Temp)}
/>
</FlexibleWidthXYPlot>
<DiscreteColorLegend
items={[
{title: "VFD Current"},
{title: "VFD Speed Feedback"},
{title: "VFD Output Power"},
{title: "VFD Temp"}
]}
orientation="horizontal"
/>
</div>
);
}
}
function mapStateToProps(state){
return {
tags: state.tags,
tagHistory: state.tagHistory
};
}
export default connect(mapStateToProps)(Main);

View File

@@ -0,0 +1,74 @@
import React, { Component } from "react";
import { connect } from "react-redux";
class Permissives extends Component {
renderTagButton = (tagName, description) => {
if (!this.props.tags[tagName]){
return(
<button
className="btn disabled"
>
{description}
</button>
);
}
const btnClassName = this.props.tags[tagName].value ? "btn btn-success" : "btn btn-danger";
return <button className={btnClassName + " disabled"}>{description}</button>;
}
render(){
if (!this.props.tags){
return (
<div>
<h2>Loading...</h2>
</div>
);
}
return(
<div>
<table id="permissives-table" className="table">
<thead>
<tr>
<th>{this.renderTagButton("sp_ALL", "START")}</th>
<th>{this.renderTagButton("rp_ALL", "RUN")}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{this.renderTagButton("sp_Flowmeter", "Flowmeter")}</td>
<td>{this.renderTagButton("rp_Flowmeter", "Flowmeter")}</td>
</tr>
<tr>
<td>{this.renderTagButton("sp_FluidLevel", "Fluid Level")}</td>
<td>{this.renderTagButton("rp_FluidLevel", "Fluid Level")}</td>
</tr>
<tr>
<td>{this.renderTagButton("sp_IntakePressure", "Intake Pressure")}</td>
<td>{this.renderTagButton("rp_IntakePressure", "Intake Pressure")}</td>
</tr>
<tr>
<td>{this.renderTagButton("sp_IntakeTemperature", "Intake Temperature")}</td>
<td>{this.renderTagButton("rp_IntakeTemperature", "Intake Temperature")}</td>
</tr>
</tbody>
</table>
</div>
);
}
}
function mapStateToProps(state){
return {
tags: state.tags
};
}
export default connect(mapStateToProps)(Permissives);

View File

@@ -0,0 +1,155 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import _ from "lodash";
import { setPlcIpAddress, ipcPlcInitializeSend } from "../actions/actions_plc";
import { storeNewTag, deleteTag } from "../actions/actions_tags";
class Settings extends Component {
constructor(props){
super(props);
this.state = {
ipAddress: "",
newTag: ""
};
}
getTagList = () => {
const { tags } = this.props;
let tableMiddle = _.map(tags, (tag) => {
return (<tr key={tag.name}>
<td>{tag.name}</td>
<td>
<button
className="btn red"
onClick={(e) => this.onDeleteClick(e, tag.name)}
><i className="material-icons">clear</i></button></td>
</tr>);
});
return (
<table>
<thead>
<tr>
<th>Tag</th>
<th></th>
</tr>
</thead>
<tbody>
{tableMiddle}
</tbody>
</table>
);
}
componentWillMount(){
console.log(this.props.plc);
if (this.props.plc && this.props.plc.ipAddress){
console.log(this.props.plc.ipAddress);
this.setState({ipAddress: this.props.plc.ipAddress});
}
}
onDeleteClick = (e, tagName) =>{
e.preventDefault();
this.props.deleteTag(tagName);
}
onIpAddressInputChange = (event) => {
this.setState({ipAddress: event.target.value});
}
sendIpAddress = (e) => {
e.preventDefault();
this.props.setPlcIpAddress(this.state.ipAddress);
}
onNewTagChange = (e) => {
this.setState({newTag: e.target.value});
}
onNewTagSubmit = (e) => {
e.preventDefault();
this.props.storeNewTag(this.state.newTag);
this.setState({newTag: ""});
}
onSave = (e) => {
console.log(this.props);
e.preventDefault();
this.props.ipcPlcInitializeSend(this.props.plc.ipAddress, this.props.tags);
this.props.history.push("/");
}
render() {
const ipAddressBtnClass = ((this.state.ipAddress === this.props.plc.ipAddress) || (this.state.ipAddress.length === 0)) ? "btn disabled right" : "btn right";
const initializeBtnClass = (this.props.plc.ipAddress && _.map(this.props.tags, (t)=>t).length > 0) ? "btn" : "btn disabled";
return (
<div style={styles.container}>
<ul className="collection with-header">
<li className="collection-header">
Settings
</li>
<form>
<li className="collection-item">
<p>IP Address</p>
<input
value={this.state.ipAddress}
placeholder="PLC IP Address"
onChange={this.onIpAddressInputChange}
/>
<button
className={ipAddressBtnClass}
onClick={(e) => this.sendIpAddress(e)}>
Set IP Address
</button>
</li>
</form>
<form>
<li className="collection-item">
<h4>Tag List</h4>
{this.getTagList()}
<input
value={this.state.newTag}
onChange={this.onNewTagChange}
placeholder="New Tag Name..."
/>
<button
className="btn"
onClick={(e) => this.onNewTagSubmit(e)}
>Add Tag</button>
</li>
<li className="collection-item right">
<button
className={initializeBtnClass}
onClick={(e) => this.onSave(e)}
>Save</button>
</li>
</form>
</ul>
</div>
);
}
}
const styles = {
container: {
display: "flex",
flexDirection: "column"
},
pointer: {
cursor: "pointer"
}
};
function mapStateToProps(state){
return{
tags: state.tags,
plc: state.plc
};
}
export default connect(mapStateToProps, { setPlcIpAddress, storeNewTag, ipcPlcInitializeSend, deleteTag })(Settings);

View File

@@ -0,0 +1,130 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import _ from "lodash";
import Gauge from "react-svg-gauge";
import { writeTag } from "../actions/actions_tags";
class TagsIndex extends Component {
constructor(props){
super(props);
this.state = {writes: {}};
}
renderTagsList(){
return _.map(this.props.tags, (t) => {
return (<tr key={t.name}>
<td>{t.name}</td>
<td>{Math.round(t.value * 100) / 100}</td>
<td><input
onChange={(e) => this.onTagWriteFieldChanged(e, t.name)}
/></td>
<td><button
className="waves-effect waves-light btn"
onClick={() => this.onWriteButtonClick(t.name)}
>Write</button></td>
</tr>);
});
}
renderTagsGauges(){
return _.map(this.props.tags, (tag) => {
return (<div className="col s4" key={tag.name}>
<Gauge value={Math.round(tag.value * 100) / 100}
width={200}
height={150}
label={tag.name}
valueLabelStyle={styles.valueLabel}
topLabelStyle={styles.topLabel}
minMaxLabelStyle={styles.minMaxLabel} />
</div>);
});
}
onWriteButtonClick = (tagName) => {
console.log(tagName, this.state.writes[tagName]);
this.props.writeTag(tagName, this.state.writes[tagName]);
}
onTagWriteFieldChanged = (e, tagName) => {
console.log(tagName, e.target.value);
this.setState({writes: {...this.state.writes, [tagName]: e.target.value}});
}
render() {
let PLC = "PLC";
if (this.props.plc.details){
PLC = this.props.plc.details.name;
}
if (!this.props.tags || _.map(this.props.tags, (t)=> t ).length === 0) {
return (
<div style={styles.container}>
<h3>
No Tags.
</h3>
<h4>Add some in <Link to="/settings">Settings</Link>.</h4>
</div>
);
}
return (
<div style={styles.container}>
<h2>Tags for {this.props.plc.ipAddress}</h2>
<h3>{PLC}</h3>
<table className="table">
<thead>
<tr>
<th>Tag Name</th>
<th>Value</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{ this.renderTagsList()}
</tbody>
</table>
</div>
);
}
}
const styles = {
container: {
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
textAlign: "center"
},
buttonContainer: {
display: "flex",
flexDirection: "column"
},
button: {
marginBottom: "15px"
},
valueLabel: {
fontSize: "1.5em"
},
topLabel: {
fontSize: "1.35em"
},
minMaxLabel: {
fontSize: "0.85em"
}
};
function mapStateToProps(state){
return {
tags: state.tags,
plc: state.plc
};
}
export default connect(mapStateToProps, { writeTag })(TagsIndex);

53
app/src/global.css Normal file
View File

@@ -0,0 +1,53 @@
/*
* Global constants goes here
*/
:root {
--primary-color: #42b983;
}
/*
* Global CSS goes here, it requires to use :global before each style
*/
:global html {
height: 100%;
}
:global body {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
margin: auto;
}
:global #app {
color: #2c3e50;
font-family: Source Sans Pro, Helvetica, sans-serif;
height: 100%;
width:100%;
}
:global #app p {
text-align: justify;
}
:global .hello {
color: var(--primary-color);
}
:global #permissives-table tbody tr td {
text-align: center;
}
:global #permissives-table tbody tr td button {
width: 95%;
}
:global #permissives-table thead tr th {
text-align: center;
}
:global #settings-button {
margin: auto 15px;
}
:global .control-button {
width: 95%;
}

15
app/src/reducers/index.js Normal file
View File

@@ -0,0 +1,15 @@
import { combineReducers } from "redux";
import TagsReducer from "./reducer_tags";
import PlcReducer from "./reducer_plc";
import TagHistoryReducer from "./reducer_taghistory";
const rootReducer = combineReducers({
tags: TagsReducer,
tagHistory: TagHistoryReducer,
plc: PlcReducer
});
export default rootReducer;

View File

@@ -0,0 +1,15 @@
import { SET_PLC_IPADDRESS, PLC_DATA_RECEIVED } from "../actions/actions_plc";
export default function(state = {}, action){
switch(action.type){
case SET_PLC_IPADDRESS:
return { ...state, ipAddress: action.payload };
case PLC_DATA_RECEIVED:
console.log(action.payload);
return { ...state, ...action.payload };
default:
return state;
}
}

View File

@@ -0,0 +1,29 @@
import _ from "lodash";
import { IPC_TAGUPDATE } from "../actions/actions_tags";
import { historyTags } from "../renderer_process";
export default function(state = {}, action){
switch (action.type) {
case IPC_TAGUPDATE:
const { name, value } = action.payload;
if (historyTags.includes(name)){
const thisEntry = {
value,
timestamp: new Date()
};
let tagHistory = [ thisEntry ];
if (state[name]){
tagHistory = _.take(_.concat(tagHistory, state[name]), 500);
}
return { ...state, [name]: tagHistory};
} else {
return state;
}
default:
return state;
}
}

View File

@@ -0,0 +1,21 @@
import _ from "lodash";
import { IPC_TAGUPDATE, STORE_NEW_TAG, DELETE_TAG } from "../actions/actions_tags";
export default function(state = {}, action){
switch (action.type) {
case IPC_TAGUPDATE:
const { name, value } = action.payload;
return { ...state, [name]: { name, value }};
case STORE_NEW_TAG:
const newTagName = action.payload;
return { ...state, [newTagName]: { name: newTagName }};
case DELETE_TAG:
return _.omit(state, action.payload);
default:
return state;
}
}

View File

@@ -0,0 +1,45 @@
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import createIpc from "redux-electron-ipc";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import reducers from "./reducers";
import TagsIndex from "./components/TagsIndex";
import Settings from "./components/Settings";
import Header from "./components/Header";
import Permissives from "./components/Permissives";
import Main from "./components/Main";
import Controls from "./components/Controls";
import { ipcTagUpdate } from "./actions/actions_tags";
import { ipcPlcDetailsReceived } from "./actions/actions_plc";
export const { history_tags: historyTags } = require("../../tagList.json");
const ipc = createIpc({
"tag:valueupdate": ipcTagUpdate,
"plc:connected": ipcPlcDetailsReceived
});
const createStoreWithMiddleware = applyMiddleware(ipc)(createStore);
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<BrowserRouter>
<div>
<Header />
<Switch>
<Route path="/settings" component={Settings} />
<Route path="/permissives" component={Permissives} />
<Route path="/alltags" component={TagsIndex} />
<Route path="/controls" component={Controls} />
<Route path="/" component={Main} />
</Switch>
</div>
</BrowserRouter>
</Provider>
, document.querySelector("#app"));