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