Initial Commit
This commit is contained in:
43
.eslintrc.js
Normal file
43
.eslintrc.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
module.exports = {
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended"
|
||||||
|
],
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"experimentalObjectRestSpread": true,
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"react"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
"error",
|
||||||
|
"tab"
|
||||||
|
],
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix"
|
||||||
|
],
|
||||||
|
"quotes": [
|
||||||
|
"error",
|
||||||
|
"double"
|
||||||
|
],
|
||||||
|
"semi": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"no-console": "off",
|
||||||
|
"react/prop-types": [0],
|
||||||
|
"no-case-declarations": "off"
|
||||||
|
}
|
||||||
|
};
|
||||||
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#
|
||||||
|
*-lock.json
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules
|
||||||
|
jspm_packages
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
dist/*
|
||||||
23
README.md
Normal file
23
README.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# node-ethernet-ip-electron
|
||||||
|
|
||||||
|
A basic electron app to read values from a PLC.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Install
|
||||||
|
``` bash
|
||||||
|
# Clone the repository
|
||||||
|
$ git clone https://github.com/patrickjmcd/node-ethernet-ip-electron
|
||||||
|
|
||||||
|
# Go into the repository
|
||||||
|
$ cd node-ethernet-ip-electron
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Develop
|
||||||
|
Just run this command to start developing with hot reloading.
|
||||||
|
``` bash
|
||||||
|
$ npm start
|
||||||
|
```
|
||||||
6293
app/build/bundle.js
Normal file
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
56
app/build/main.css
Normal 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
16
app/index.html
Normal 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>
|
||||||
30
app/src/actions/actions_plc.js
Normal file
30
app/src/actions/actions_plc.js
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
46
app/src/actions/actions_tags.js
Normal file
46
app/src/actions/actions_tags.js
Normal 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
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
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
BIN
app/src/assets/webpack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
260
app/src/components/Controls.js
Normal file
260
app/src/components/Controls.js
Normal 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);
|
||||||
76
app/src/components/Header.js
Normal file
76
app/src/components/Header.js
Normal 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
101
app/src/components/Main.js
Normal 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);
|
||||||
74
app/src/components/Permissives.js
Normal file
74
app/src/components/Permissives.js
Normal 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);
|
||||||
155
app/src/components/Settings.js
Normal file
155
app/src/components/Settings.js
Normal 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);
|
||||||
130
app/src/components/TagsIndex.js
Normal file
130
app/src/components/TagsIndex.js
Normal 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
53
app/src/global.css
Normal 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
15
app/src/reducers/index.js
Normal 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;
|
||||||
15
app/src/reducers/reducer_plc.js
Normal file
15
app/src/reducers/reducer_plc.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/src/reducers/reducer_taghistory.js
Normal file
29
app/src/reducers/reducer_taghistory.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/src/reducers/reducer_tags.js
Normal file
21
app/src/reducers/reducer_tags.js
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/src/renderer_process.js
Normal file
45
app/src/renderer_process.js
Normal 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"));
|
||||||
74
main_process.js
Normal file
74
main_process.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// Basic init
|
||||||
|
const electron = require("electron");
|
||||||
|
const { Controller, Tag } = require("ethernet-ip");
|
||||||
|
const _ = require("lodash");
|
||||||
|
const { app, BrowserWindow, ipcMain } = electron;
|
||||||
|
|
||||||
|
// To avoid being garbage collected
|
||||||
|
let mainWindow;
|
||||||
|
let PLC;
|
||||||
|
|
||||||
|
const tagList = require("./tagList.json");
|
||||||
|
|
||||||
|
app.on("ready", () => {
|
||||||
|
|
||||||
|
mainWindow = new BrowserWindow({});
|
||||||
|
mainWindow.loadURL(`file://${__dirname}/app/index.html`);
|
||||||
|
|
||||||
|
initPLC("10.20.4.36", tagList.scan_list);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
function writeTag(tagName, tagValue){
|
||||||
|
const thisTag = new Tag(tagName);
|
||||||
|
PLC.readTag(thisTag).then(() => {
|
||||||
|
thisTag.value = tagValue;
|
||||||
|
PLC.writeTag(thisTag);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPLC(ipAddress, tagList){
|
||||||
|
PLC = new Controller();
|
||||||
|
|
||||||
|
const setupTags = new Promise ((resolve) =>{
|
||||||
|
resolve(_.map(tagList, (tag) => {
|
||||||
|
PLC.subscribe(new Tag( tag ));
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
setupTags.then(()=>{
|
||||||
|
PLC.connect(ipAddress, 0).then( () => {
|
||||||
|
const properties = { ...PLC.properties, ipAddress};
|
||||||
|
mainWindow.webContents.send("plc:connected", properties);
|
||||||
|
PLC.scan().catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
PLC.forEach( (tag) => {
|
||||||
|
tag.on("Initialized", (tag) => {
|
||||||
|
// console.log("main_process: Initialized", tag.name, tag.value);
|
||||||
|
mainWindow.webContents.send("tag:valueupdate", tag);
|
||||||
|
});
|
||||||
|
|
||||||
|
tag.on("Changed", (tag) => {
|
||||||
|
// console.log("main_process: Changed", tag.name, tag.value);
|
||||||
|
mainWindow.webContents.send("tag:valueupdate", tag);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ipcMain.on("plc:initialize", (event, ipAddress, tagList) =>{
|
||||||
|
// console.log("plc:initialize", ipAddress, tagList);
|
||||||
|
initPLC(ipAddress, tagList);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on("tag:write", (event, tagName, value) => {
|
||||||
|
// console.log("tag:write", tagName, value);
|
||||||
|
writeTag(tagName, value);
|
||||||
|
});
|
||||||
|
|
||||||
63
package.json
Normal file
63
package.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"name": "maxwatersystem-electron",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"description": "Electron-based HMI Program",
|
||||||
|
"main": "main_process.js",
|
||||||
|
"scripts": {
|
||||||
|
"bundle": "webpack --mode development",
|
||||||
|
"wpackserve": "webpack --mode development -w",
|
||||||
|
"serve": "electron .",
|
||||||
|
"start": "npm-run-all --parallel wpackserve serve",
|
||||||
|
"pack": "electron-builder --dir",
|
||||||
|
"dist": "electron-builder -mwl"
|
||||||
|
},
|
||||||
|
"author": "Patrick J. McDonagh",
|
||||||
|
"repository": "HenryPump/MaxWaterSystem-Electron",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.26.0",
|
||||||
|
"babel-eslint": "^8.2.2",
|
||||||
|
"babel-loader": "^7.1.4",
|
||||||
|
"babel-preset-env": "^1.6.1",
|
||||||
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"babel-preset-stage-0": "^6.24.1",
|
||||||
|
"css-loader": "^0.28.10",
|
||||||
|
"electron": "^2.0.0-beta.5",
|
||||||
|
"electron-builder": "^20.8.1",
|
||||||
|
"electron-reload": "^1.2.2",
|
||||||
|
"eslint": "^4.19.1",
|
||||||
|
"eslint-plugin-react": "^7.7.0",
|
||||||
|
"file-loader": "^1.1.10",
|
||||||
|
"mini-css-extract-plugin": "^0.4.0",
|
||||||
|
"npm-run-all": "^4.1.2",
|
||||||
|
"webpack": "^4.1.1",
|
||||||
|
"webpack-cli": "^2.0.11"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome": "^1.1.5",
|
||||||
|
"@fortawesome/fontawesome-pro-regular": "^5.0.9",
|
||||||
|
"@fortawesome/fontawesome-pro-webfonts": "^1.0.5",
|
||||||
|
"@fortawesome/react-fontawesome": "0.0.18",
|
||||||
|
"ethernet-ip": "^1.1.4",
|
||||||
|
"lodash": "^4.17.5",
|
||||||
|
"react": "^16.2.0",
|
||||||
|
"react-dom": "^16.2.0",
|
||||||
|
"react-redux": "^5.0.7",
|
||||||
|
"react-router": "^4.2.0",
|
||||||
|
"react-router-dom": "^4.2.2",
|
||||||
|
"react-svg-gauge": "^1.0.7",
|
||||||
|
"react-vis": "^1.9.2",
|
||||||
|
"redux": "^3.7.2",
|
||||||
|
"redux-electron-ipc": "^1.1.12",
|
||||||
|
"victory": "^0.25.7"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.henrypump.maxwatersystem",
|
||||||
|
"mac": {
|
||||||
|
"target": "default"
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": "portable"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
tagList.json
Normal file
105
tagList.json
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
{
|
||||||
|
"scan_list":
|
||||||
|
[
|
||||||
|
"alarm_ESTOP",
|
||||||
|
"alarm_Flowmeter",
|
||||||
|
"alarm_FluidLevel",
|
||||||
|
"alarm_IntakePressure",
|
||||||
|
"alarm_IntakeTemperature",
|
||||||
|
"alarm_Lockout",
|
||||||
|
"alarm_MinSpeed",
|
||||||
|
"alarm_TubingPressure",
|
||||||
|
"alarm_VFD",
|
||||||
|
"cfg_CostPerkWh",
|
||||||
|
"cfg_CurrentLimitMultiplier",
|
||||||
|
"cfg_DHSensorDistToIntake",
|
||||||
|
"cfg_DHSensorPressureOffset",
|
||||||
|
"cfg_FluidSpecificGravity",
|
||||||
|
"cfg_MinSpeedSecondsBeforeFault",
|
||||||
|
"cfg_PID_Flow",
|
||||||
|
"cfg_PID_FlowSP",
|
||||||
|
"cfg_PID_FluidLevel",
|
||||||
|
"cfg_PID_FluidLevelSP",
|
||||||
|
"cfg_PID_Manual",
|
||||||
|
"cfg_PID_ManualSP",
|
||||||
|
"cfg_PID_TubingPressure",
|
||||||
|
"cfg_PID_TubingPressureSP",
|
||||||
|
"cmd_Start",
|
||||||
|
"cmd_Stop",
|
||||||
|
"Device_Status_INT",
|
||||||
|
"Downhole_Sensor_Status_INT",
|
||||||
|
"Flow_Total_LastMonth",
|
||||||
|
"Flow_Total_Lifetime",
|
||||||
|
"Flow_Total_ThisMonth",
|
||||||
|
"rp_ALL",
|
||||||
|
"rp_Flowmeter",
|
||||||
|
"rp_FluidLevel",
|
||||||
|
"rp_IntakePressure",
|
||||||
|
"rp_IntakeTemperature",
|
||||||
|
"rp_MinSpeed",
|
||||||
|
"rp_TubingPressure",
|
||||||
|
"rp_VFD",
|
||||||
|
"sp_ALL",
|
||||||
|
"sp_Flowmeter",
|
||||||
|
"sp_FluidLevel",
|
||||||
|
"sp_IntakePressure",
|
||||||
|
"sp_IntakeTemperature",
|
||||||
|
"sp_Time",
|
||||||
|
"sp_TubingPressure",
|
||||||
|
"sp_VFD",
|
||||||
|
"sts_CurrentVFDFaultCode",
|
||||||
|
"sts_NoAlarms",
|
||||||
|
"sts_PID_Control",
|
||||||
|
"sts_PumpOff",
|
||||||
|
"sts_RestartAllowed",
|
||||||
|
"sts_TrueAlarm",
|
||||||
|
"sts_WaitingToRestart",
|
||||||
|
"time_RunTime_Hours",
|
||||||
|
"time_TotalSecondsUntilStartup",
|
||||||
|
"val_Flowmeter",
|
||||||
|
"val_Flowmeter_BarrelsPerDay",
|
||||||
|
"val_FluidLevel",
|
||||||
|
"val_IntakePressure",
|
||||||
|
"val_IntakeTemperature",
|
||||||
|
"val_TubingPressure",
|
||||||
|
"VFD_MotorNPAmps",
|
||||||
|
"VFD_MotorNPHertz",
|
||||||
|
"VFD_MotorNPHorsepower",
|
||||||
|
"VFD_MotorNPOLFactor",
|
||||||
|
"VFD_MotorNPRPM",
|
||||||
|
"VFD_MotorNPVolts",
|
||||||
|
"VFD_MotorPoles",
|
||||||
|
"VFD_OutCurrent",
|
||||||
|
"VFD_OutPower",
|
||||||
|
"VFD_PWMFrequency",
|
||||||
|
"VFD_SpeedFdbk",
|
||||||
|
"VFD_SpeedRef",
|
||||||
|
"VFD_Temp"
|
||||||
|
],
|
||||||
|
"history_tags":
|
||||||
|
[
|
||||||
|
"val_Flowmeter",
|
||||||
|
"val_Flowmeter_BarrelsPerDay",
|
||||||
|
"val_FluidLevel",
|
||||||
|
"val_IntakePressure",
|
||||||
|
"val_IntakeTemperature",
|
||||||
|
"val_TubingPressure",
|
||||||
|
"VFD_OutCurrent",
|
||||||
|
"VFD_OutPower",
|
||||||
|
"VFD_SpeedFdbk",
|
||||||
|
"VFD_SpeedRef",
|
||||||
|
"VFD_Temp"
|
||||||
|
],
|
||||||
|
"alarm_tags":
|
||||||
|
[
|
||||||
|
"alarm_ESTOP",
|
||||||
|
"alarm_Flowmeter",
|
||||||
|
"alarm_FluidLevel",
|
||||||
|
"alarm_IntakePressure",
|
||||||
|
"alarm_IntakeTemperature",
|
||||||
|
"alarm_Lockout",
|
||||||
|
"alarm_MinSpeed",
|
||||||
|
"alarm_TubingPressure",
|
||||||
|
"alarm_VFD"
|
||||||
|
]
|
||||||
|
}
|
||||||
60
webpack.config.js
Normal file
60
webpack.config.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
|
module.exports = {
|
||||||
|
|
||||||
|
watch: false,
|
||||||
|
target: "electron-main",
|
||||||
|
entry: ["./app/src/renderer_process.js", "./app/src/global.css"],
|
||||||
|
output: {
|
||||||
|
path: __dirname + "/app/build",
|
||||||
|
publicPath: "build/",
|
||||||
|
filename: "bundle.js"
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.jsx?$/,
|
||||||
|
loader: "babel-loader",
|
||||||
|
exclude: /node_modules/,
|
||||||
|
options: {
|
||||||
|
presets: [ "es2015", "react", "stage-0" ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// test: /\.css$/,
|
||||||
|
// loader: ExtractTextPlugin.extract({
|
||||||
|
// loader: "css-loader",
|
||||||
|
// options: {
|
||||||
|
// modules: true
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
MiniCssExtractPlugin.loader,
|
||||||
|
"css-loader"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jpg|gif|svg)$/,
|
||||||
|
loader: "file-loader",
|
||||||
|
query: {
|
||||||
|
name: "[name].[ext]?[hash]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: "main.css",
|
||||||
|
chunkFilename: "[id].css"
|
||||||
|
})
|
||||||
|
],
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
extensions: [".js", ".json", ".jsx"]
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user