Improve repo directory structure
50
.eslintignore
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# 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
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Dependency directory
|
||||||
|
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# App packaged
|
||||||
|
release
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
dll
|
||||||
|
main.js
|
||||||
|
main.js.map
|
||||||
|
|
||||||
|
.idea
|
||||||
|
npm-debug.log.*
|
||||||
|
__snapshots__
|
||||||
|
|
||||||
|
# Package.json
|
||||||
|
package.json
|
||||||
|
.travis.yml
|
||||||
|
*.css.d.ts
|
||||||
|
*.sass.d.ts
|
||||||
|
*.scss.d.ts
|
49
.eslintrc.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: ["plugin:react/recommended", "airbnb"],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
ecmaVersion: 12,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
plugins: ["react", "@typescript-eslint"],
|
||||||
|
rules: {
|
||||||
|
"no-use-before-define": "off",
|
||||||
|
"@typescript-eslint/no-use-before-define": ["error"],
|
||||||
|
"react/jsx-filename-extension": [1, { extensions: [".tsx", ".ts"] }],
|
||||||
|
"consistent-return": ["error", { treatUndefinedAsUnspecified: false }],
|
||||||
|
quotes: ["error", "double"],
|
||||||
|
"import/no-extraneous-dependencies": ["error", { devDependencies: true }],
|
||||||
|
"no-console": "off",
|
||||||
|
"max-len": ["error", { code: 300 }],
|
||||||
|
"no-shadow": "off",
|
||||||
|
"@typescript-eslint/no-shadow": ["error"],
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
|
"import/extensions": [
|
||||||
|
"error",
|
||||||
|
"ignorePackages",
|
||||||
|
{
|
||||||
|
js: "never",
|
||||||
|
jsx: "never",
|
||||||
|
ts: "never",
|
||||||
|
tsx: "never",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
"import/resolver": {
|
||||||
|
node: {
|
||||||
|
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
};
|
40
.gitignore
vendored
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
npm-debug.log
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# App packaged
|
||||||
|
release
|
||||||
|
dist
|
||||||
|
dll
|
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": false
|
||||||
|
}
|
22
README.md
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# Heliox - Dashboard
|
||||||
|
|
||||||
|
[![Electron Build](https://github.com/GHOSCHT/light-control/actions/workflows/Electron.yml/badge.svg)](https://github.com/GHOSCHT/light-control/actions/workflows/Electron.yml)
|
||||||
|
[![CodeQL](https://github.com/GHOSCHT/light-control/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/GHOSCHT/light-control/actions/workflows/codeql-analysis.yml)
|
||||||
|
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/bdb8a994396345efab8271307f1ea155)](https://www.codacy.com/gh/GHOSCHT/heliox/dashboard?utm_source=github.com&utm_medium=referral&utm_content=GHOSCHT/heliox&utm_campaign=Badge_Grade)
|
||||||
|
<a href="https://www.figma.com/file/fK5tEIw4Zx8AivuVbu79Lw/Light-Control">
|
||||||
|
<img src="https://img.shields.io/badge/Figma-F24E1E?style=flat&logo=figma&logoColor=white" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
Uses: <https://github.com/GHOSCHT/simple-electron-react-boilerplate>
|
||||||
|
|
||||||
|
Dashboard: node-gyp fails on sqlite3 build -> set default pyhton version to python 2
|
||||||
|
|
||||||
|
```shell
|
||||||
|
node-gyp --python /path/to/python2.7/python.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm config set python /path/to/executable/python2.7
|
||||||
|
```
|
4
assets/assets.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
declare module "*.svg" {
|
||||||
|
const content: any;
|
||||||
|
export default content;
|
||||||
|
}
|
6630
assets/icons/Icon.ai
Normal file
BIN
assets/icons/Icon.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
assets/icons/mac/icon.icns
Normal file
BIN
assets/icons/png/1024x1024.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
assets/icons/png/128x128.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
assets/icons/png/16x16.png
Normal file
After Width: | Height: | Size: 424 B |
BIN
assets/icons/png/24x24.png
Normal file
After Width: | Height: | Size: 597 B |
BIN
assets/icons/png/256x256.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
BIN
assets/icons/png/32x32.png
Normal file
After Width: | Height: | Size: 795 B |
BIN
assets/icons/png/48x48.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/icons/png/512x512.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
assets/icons/png/64x64.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/icons/win/icon.ico
Normal file
After Width: | Height: | Size: 353 KiB |
6605
assets/knob/Knob.ai
Normal file
1
assets/knob/Knob.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 141.73 141.73"><defs><style>.cls-1{fill:#545353}.cls-2{font-size:20px;fill:#fff;font-family:SegoeUI-Bold,Segoe UI;font-weight:700}.cls-3{fill:#2f6087}</style></defs><title>Knob</title><g id="Base"><path d="M352.12,273A70.87,70.87,0,1,0,423,343.84,70.86,70.86,0,0,0,352.12,273Zm0,120.48a49.61,49.61,0,1,1,49.61-49.61A49.61,49.61,0,0,1,352.12,393.45Z" class="cls-1" transform="translate(-281.26 -272.97)"/></g><g id="Status"><text class="cls-2" transform="translate(53.61 77.41)">255</text></g><g id="Overlay"><g><path d="M363.54,295.57" class="cls-3" transform="translate(-281.26 -272.97)"/><path d="M363.54,295.57a49.44,49.44,0,0,0-11.42-1.34" class="cls-3" transform="translate(-281.26 -272.97)"/><path d="M352.12,294.23" class="cls-3" transform="translate(-281.26 -272.97)"/><path d="M368.76,275l-.33-.1v0Z" class="cls-3" transform="translate(-281.26 -272.97)"/><polygon points="87.18 1.89 87.18 1.89 87.17 1.91 87.17 1.91 87.18 1.89" class="cls-3"/><polygon points="82.28 22.58 87.17 1.91 87.17 1.91 82.28 22.58 82.28 22.58" class="cls-3"/><path d="M368.76,275l-.33-.08A71.24,71.24,0,0,0,352.12,273a10.71,10.71,0,0,0-10.53,10.73,10.53,10.53,0,0,0,10.53,10.53,49.44,49.44,0,0,1,11.42,1.34v0a10.33,10.33,0,0,0,12.56-7.68A10.79,10.79,0,0,0,368.76,275Z" class="cls-3" transform="translate(-281.26 -272.97)"/></g></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
367
assets/tray/Swatch.ai
Normal file
1879
assets/tray/Template.ai
Normal file
BIN
assets/tray/tray-default-32.png
Normal file
After Width: | Height: | Size: 451 B |
BIN
assets/tray/tray-green-32.png
Normal file
After Width: | Height: | Size: 612 B |
BIN
assets/tray/tray-red-32.png
Normal file
After Width: | Height: | Size: 614 B |
BIN
assets/tray/tray-yellow-32.png
Normal file
After Width: | Height: | Size: 601 B |
375
assets/tray/tray.ai
Normal file
4
babel.config.json
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"],
|
||||||
|
"plugins": ["@babel/plugin-proposal-class-properties"]
|
||||||
|
}
|
121
package.json
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
{
|
||||||
|
"name": "dashboard",
|
||||||
|
"author": {
|
||||||
|
"name": "GHOSCHT"
|
||||||
|
},
|
||||||
|
"description": "",
|
||||||
|
"version": "2.2.1",
|
||||||
|
"main": "src/electron.js",
|
||||||
|
"private": true,
|
||||||
|
"build": {
|
||||||
|
"productName": "Heliox",
|
||||||
|
"appId": "heliox.dashboard",
|
||||||
|
"buildDependenciesFromSource": true,
|
||||||
|
"npmRebuild": false,
|
||||||
|
"win": {
|
||||||
|
"target": [
|
||||||
|
"nsis"
|
||||||
|
],
|
||||||
|
"icon": "./assets/icons/win/icon.ico"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true,
|
||||||
|
"include": "./scripts/installer.nsh"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "assets",
|
||||||
|
"output": "release"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"node_modules/**/*",
|
||||||
|
"src/*",
|
||||||
|
"package.json",
|
||||||
|
"build/**"
|
||||||
|
],
|
||||||
|
"extraResources": [
|
||||||
|
"./assets/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"homepage": "./src",
|
||||||
|
"scripts": {
|
||||||
|
"web": "webpack serve --port 8420",
|
||||||
|
"build": "webpack --mode=production",
|
||||||
|
"start": "concurrently -k \"yarn web\" \"npm:electron\"",
|
||||||
|
"electron": "wait-on tcp:8420 && electron .",
|
||||||
|
"package": "yarn build && electron-builder --publish never",
|
||||||
|
"postinstall": "electron-builder install-app-deps",
|
||||||
|
"lint": "cross-env NODE_ENV=development eslint . --cache --ext .js,.jsx,.ts,.tsx",
|
||||||
|
"lint:fix": "cross-env NODE_ENV=development eslint . --cache --fix --ext .js,.jsx,.ts,.tsx",
|
||||||
|
"remotedev": "redux-devtools --hostname=localhost --port=8000"
|
||||||
|
},
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@fluentui/react": "^8.61.0",
|
||||||
|
"@fluentui/react-icons-mdl2": "^1.3.4",
|
||||||
|
"@reduxjs/toolkit": "^1.7.1",
|
||||||
|
"@types/jest": "^27.0.2",
|
||||||
|
"@types/node": "^15.14.9",
|
||||||
|
"@types/react": "^17.0.27",
|
||||||
|
"@types/react-dom": "^17.0.9",
|
||||||
|
"@types/react-redux": "^7.1.21",
|
||||||
|
"@types/remote-redux-devtools": "^0.5.5",
|
||||||
|
"@types/styled-components": "^5.1.14",
|
||||||
|
"chalk": "^4.1.2",
|
||||||
|
"electron-acrylic-window": "^0.5.9",
|
||||||
|
"electron-devtools-installer": "^3.2.0",
|
||||||
|
"electron-is-dev": "^2.0.0",
|
||||||
|
"electron-localshortcut": "^3.2.1",
|
||||||
|
"electron-squirrel-startup": "^1.0.0",
|
||||||
|
"electron-store": "^8.0.1",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-redux": "^7.2.5",
|
||||||
|
"react-select": "^5.1.0",
|
||||||
|
"redux": "^4.1.1",
|
||||||
|
"redux-persist": "^6.0.0",
|
||||||
|
"redux-persist-electron-storage": "^2.1.0",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
|
"screenz": "^1.0.0",
|
||||||
|
"serialport": "^9.2.4",
|
||||||
|
"styled-components": "^5.3.1",
|
||||||
|
"typescript": "^4.4.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.15.5",
|
||||||
|
"@babel/preset-env": "^7.15.6",
|
||||||
|
"@babel/preset-react": "^7.14.5",
|
||||||
|
"@types/react-select": "^5.0.0",
|
||||||
|
"@types/serialport": "^8.0.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||||
|
"@typescript-eslint/parser": "^4.33.0",
|
||||||
|
"babel-loader": "^8.2.2",
|
||||||
|
"concurrently": "^6.3.0",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"css-loader": "^6.3.0",
|
||||||
|
"electron": "^13.5.1",
|
||||||
|
"electron-builder": "^22.11.7",
|
||||||
|
"eslint": "^7.32.0",
|
||||||
|
"eslint-config-airbnb": "^18.2.1",
|
||||||
|
"eslint-plugin-import": "^2.24.2",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
|
"eslint-plugin-react": "^7.26.1",
|
||||||
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
|
"file-loader": "^6.2.0",
|
||||||
|
"fork-ts-checker-webpack-plugin": "^6.3.3",
|
||||||
|
"html-webpack-plugin": "^5.3.2",
|
||||||
|
"style-loader": "^3.3.0",
|
||||||
|
"ts-loader": "^9.2.6",
|
||||||
|
"wait-on": "^6.0.0",
|
||||||
|
"webpack": "^5.57.1",
|
||||||
|
"webpack-cli": "^4.9.2",
|
||||||
|
"webpack-dev-server": "^4.7.4"
|
||||||
|
},
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
"@babel/preset-react"
|
||||||
|
],
|
||||||
|
"bin": {
|
||||||
|
"dashboard": "./bin/start.js"
|
||||||
|
}
|
||||||
|
}
|
3
scripts/installer.nsh
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
!macro customInstall
|
||||||
|
CreateShortcut "$SMSTARTUP\Heliox.lnk" "$INSTDIR\Heliox.exe"
|
||||||
|
!macroend
|
35
src/App.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import React from "react";
|
||||||
|
import { createGlobalStyle } from "styled-components";
|
||||||
|
import { registerIcons } from "@fluentui/react/lib/Styling";
|
||||||
|
import { ChevronDownIcon, CancelIcon } from "@fluentui/react-icons-mdl2";
|
||||||
|
import KnobSection from "./Components/KnobSection";
|
||||||
|
import Settings from "./Components/Settings";
|
||||||
|
|
||||||
|
const GlobalStyle = createGlobalStyle`
|
||||||
|
html {
|
||||||
|
border-style: solid;
|
||||||
|
border-color: #363636;
|
||||||
|
border-width: 1px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
border-bottom-style: hidden;
|
||||||
|
border-right-style: hidden;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
registerIcons({
|
||||||
|
icons: {
|
||||||
|
ChevronDown: <ChevronDownIcon />,
|
||||||
|
Cancel: <CancelIcon />,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Settings />
|
||||||
|
<GlobalStyle />
|
||||||
|
<KnobSection />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default App;
|
157
src/Components/Knob.tsx
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { remote } from "electron";
|
||||||
|
|
||||||
|
const SvgHeight = 180;
|
||||||
|
const steps = 5;
|
||||||
|
const accentColor = remote.systemPreferences.getAccentColor();
|
||||||
|
|
||||||
|
const KnobSVG = styled.svg`
|
||||||
|
.cls-1 {
|
||||||
|
fill: #545353;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
font-size: 20px;
|
||||||
|
fill: #fff;
|
||||||
|
font-family: SegoeUI-Bold, Segoe UI;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-3 {
|
||||||
|
//TODO: change to styled theme
|
||||||
|
fill: rgba(${parseInt(accentColor.substr(0, 2), 16)}, ${parseInt(accentColor.substr(2, 2), 16)}, ${parseInt(accentColor.substr(4, 2), 16)},${parseInt(accentColor.substr(6, 8), 16)});
|
||||||
|
}
|
||||||
|
#Status {
|
||||||
|
user-select: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Proptypes {
|
||||||
|
increase: () => void;
|
||||||
|
decrease: () => void;
|
||||||
|
toggle: () => void;
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Knob = ({
|
||||||
|
increase, decrease, toggle, status,
|
||||||
|
}: Proptypes) => {
|
||||||
|
const overlayRef = useRef<SVGGElement>(null);
|
||||||
|
const [isMouseDown, setisMouseDown] = useState(false);
|
||||||
|
const [prevAngle, setPrevAngle] = useState(0);
|
||||||
|
|
||||||
|
const move = (e: React.MouseEvent<SVGElement, MouseEvent>) => {
|
||||||
|
const relativePosX = e.nativeEvent.offsetX - SvgHeight / 2;
|
||||||
|
const relativePosY = e.nativeEvent.offsetY - SvgHeight / 2;
|
||||||
|
|
||||||
|
const angleDegree = Math.atan2(relativePosY, relativePosX) * (180 / Math.PI);
|
||||||
|
|
||||||
|
let angle = 0;
|
||||||
|
angle = angleDegree + 85;
|
||||||
|
if (angle < 0) {
|
||||||
|
angle = 360 + angle;
|
||||||
|
} else if (angle > 360) {
|
||||||
|
angle = 360 - angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlayRef.current !== null) {
|
||||||
|
overlayRef.current.style.transformOrigin = "50% 50%";
|
||||||
|
overlayRef.current.style.transform = `rotate(${angle}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (angle % steps === Math.round(angle % steps)) {
|
||||||
|
if (prevAngle < angle) {
|
||||||
|
increase();
|
||||||
|
} else if (prevAngle > angle) {
|
||||||
|
decrease();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setPrevAngle(angle);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mouseDownHandler: React.MouseEventHandler<SVGElement> = (e) => {
|
||||||
|
if (e.button !== 2) {
|
||||||
|
move(e);
|
||||||
|
setisMouseDown(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mouseUpHandler: React.MouseEventHandler<SVGElement> = () => {
|
||||||
|
setisMouseDown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mouseMoveHandler: React.MouseEventHandler<SVGElement> = (e) => {
|
||||||
|
if (isMouseDown) {
|
||||||
|
move(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<KnobSVG
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 141.73 141.73"
|
||||||
|
height={`${SvgHeight}px`}
|
||||||
|
onContextMenu={() => { toggle(); }}
|
||||||
|
onMouseDown={mouseDownHandler}
|
||||||
|
onMouseUp={mouseUpHandler}
|
||||||
|
onMouseMove={mouseMoveHandler}
|
||||||
|
>
|
||||||
|
<g id="Base">
|
||||||
|
<path
|
||||||
|
className="cls-1"
|
||||||
|
d="M352.12,273A70.87,70.87,0,1,0,423,343.84,70.86,70.86,0,0,0,352.12,273Zm0,120.48a49.61,49.61,0,1,1,49.61-49.61A49.61,49.61,0,0,1,352.12,393.45Z"
|
||||||
|
transform="translate(-281.26 -272.97)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g id="Status">
|
||||||
|
<text className="cls-2" transform="translate(70.41 77.41)" textAnchor="middle">
|
||||||
|
{status}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
<g id="Overlay" ref={overlayRef}>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
className="cls-3"
|
||||||
|
d="M363.54,295.57"
|
||||||
|
transform="translate(-281.26 -272.97)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-3"
|
||||||
|
d="M363.54,295.57a49.44,49.44,0,0,0-11.42-1.34"
|
||||||
|
transform="translate(-281.26 -272.97)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-3"
|
||||||
|
d="M352.12,294.23"
|
||||||
|
transform="translate(-281.26 -272.97)"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-3"
|
||||||
|
d="M368.76,275l-.33-.1v0Z"
|
||||||
|
transform="translate(-281.26 -272.97)"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
className="cls-3"
|
||||||
|
points="87.18 1.89 87.18 1.89 87.17 1.91 87.17 1.91 87.18 1.89"
|
||||||
|
/>
|
||||||
|
<polygon
|
||||||
|
className="cls-3"
|
||||||
|
points="82.28 22.58 87.17 1.91 87.17 1.91 82.28 22.58 82.28 22.58"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="cls-3"
|
||||||
|
d="M368.76,275l-.33-.08A71.24,71.24,0,0,0,352.12,273a10.71,10.71,0,0,0-10.53,10.73,10.53,10.53,0,0,0,10.53,10.53,49.44,49.44,0,0,1,11.42,1.34v0a10.33,10.33,0,0,0,12.56-7.68A10.79,10.79,0,0,0,368.76,275Z"
|
||||||
|
transform="translate(-281.26 -272.97)"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</KnobSVG>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Knob;
|
44
src/Components/KnobSection.tsx
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import React from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../redux/hooks";
|
||||||
|
import { sendMessage } from "../redux/slices/serialConnectionSlice";
|
||||||
|
import Knob from "./Knob";
|
||||||
|
|
||||||
|
const KnobContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 1rem;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ids: number[] = [0, 1, 2, 3];
|
||||||
|
|
||||||
|
const KnobSection = () => {
|
||||||
|
const serialConnection = useAppSelector((state) => state.serialConnection);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const sendIncreaseHandler = (index: number) => {
|
||||||
|
dispatch(sendMessage(`${index}i`));
|
||||||
|
console.log(index);
|
||||||
|
};
|
||||||
|
const sendDecreaseHandler = (index: number) => {
|
||||||
|
dispatch(sendMessage(`${index}d`));
|
||||||
|
console.log(index);
|
||||||
|
};
|
||||||
|
const sendToggleHandler = (index: number) => {
|
||||||
|
dispatch(sendMessage(`${index}t`));
|
||||||
|
console.log(index);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<KnobContainer>
|
||||||
|
{ids.map((id) => (
|
||||||
|
<Knob
|
||||||
|
key={id}
|
||||||
|
increase={() => { sendIncreaseHandler(id); }}
|
||||||
|
decrease={() => { sendDecreaseHandler(id); }}
|
||||||
|
toggle={() => { sendToggleHandler(id); }}
|
||||||
|
status={Math.floor((parseInt(serialConnection.message.split(",")[id], 10) / 255) * 100)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</KnobContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default KnobSection;
|
104
src/Components/Settings.tsx
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import electron from "electron";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Dialog, DialogType, DialogFooter } from "@fluentui/react/lib/Dialog";
|
||||||
|
import { DefaultButton } from "@fluentui/react/lib/Button";
|
||||||
|
import { ThemeProvider, PartialTheme } from "@fluentui/react/lib/Theme";
|
||||||
|
import { ComboBox, IComboBoxOption } from "@fluentui/react/lib/ComboBox";
|
||||||
|
import SerialPort from "serialport";
|
||||||
|
import { useAppDispatch, useAppSelector } from "../redux/hooks";
|
||||||
|
import { connect, disconnect, setSerialPort } from "../redux/slices/serialConnectionSlice";
|
||||||
|
|
||||||
|
type Proptypes = {
|
||||||
|
}
|
||||||
|
const dialogStyles = { main: { maxWidth: 450 } };
|
||||||
|
|
||||||
|
const myTheme: PartialTheme = {
|
||||||
|
palette: {
|
||||||
|
themePrimary: "#0078d4",
|
||||||
|
themeLighterAlt: "#eff6fc",
|
||||||
|
themeLighter: "#deecf9",
|
||||||
|
themeLight: "#c7e0f4",
|
||||||
|
themeTertiary: "#71afe5",
|
||||||
|
themeSecondary: "#2b88d8",
|
||||||
|
themeDarkAlt: "#106ebe",
|
||||||
|
themeDark: "#005a9e",
|
||||||
|
themeDarker: "#004578",
|
||||||
|
neutralLighterAlt: "#282828",
|
||||||
|
neutralLighter: "#313131",
|
||||||
|
neutralLight: "#3f3f3f",
|
||||||
|
neutralQuaternaryAlt: "#484848",
|
||||||
|
neutralQuaternary: "#4f4f4f",
|
||||||
|
neutralTertiaryAlt: "#6d6d6d",
|
||||||
|
neutralTertiary: "#c8c8c8",
|
||||||
|
neutralSecondary: "#d0d0d0",
|
||||||
|
neutralPrimaryAlt: "#dadada",
|
||||||
|
neutralPrimary: "#ffffff",
|
||||||
|
neutralDark: "#f4f4f4",
|
||||||
|
black: "#f8f8f8",
|
||||||
|
white: "#1f1f1f",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const modalProps = {
|
||||||
|
titleAriaId: "labelId",
|
||||||
|
subtitleAriaId: "subTextId",
|
||||||
|
isBlocking: false,
|
||||||
|
styles: dialogStyles,
|
||||||
|
};
|
||||||
|
const dialogContentProps = {
|
||||||
|
type: DialogType.close,
|
||||||
|
title: "Settings",
|
||||||
|
closeButtonAriaLabel: "Close",
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: IComboBoxOption[] = [
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchPorts = async () => {
|
||||||
|
const data = await SerialPort.list();
|
||||||
|
options.splice(0, options.length);
|
||||||
|
data.forEach((comPort) => { options.push({ key: comPort.path, text: comPort.path }); });
|
||||||
|
};
|
||||||
|
|
||||||
|
const Settings: React.FC<Proptypes> = () => {
|
||||||
|
const [hidden, setHidden] = useState(true);
|
||||||
|
const ipcHandler = () => {
|
||||||
|
setHidden(false);
|
||||||
|
};
|
||||||
|
electron.ipcRenderer.on("show-settings", ipcHandler);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handlePortChange = async (option: IComboBoxOption | undefined) => {
|
||||||
|
if (option !== null && option !== undefined) {
|
||||||
|
dispatch(setSerialPort(option.key.toString()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchPorts();
|
||||||
|
const selector = useAppSelector((state) => state);
|
||||||
|
console.log(selector.serialConnection.port);
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={myTheme}>
|
||||||
|
<Dialog
|
||||||
|
hidden={hidden}
|
||||||
|
onDismiss={() => { setHidden(true); }}
|
||||||
|
dialogContentProps={dialogContentProps}
|
||||||
|
modalProps={modalProps}
|
||||||
|
>
|
||||||
|
<ComboBox
|
||||||
|
label="COM Port"
|
||||||
|
options={options}
|
||||||
|
onMenuOpen={() => { fetchPorts(); }}
|
||||||
|
onChange={(_, option) => { handlePortChange(option); }}
|
||||||
|
selectedKey={selector.serialConnection.port}
|
||||||
|
disabled={selector.serialConnection.status.connected}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DefaultButton primary disabled={selector.serialConnection.status.connected} onClick={() => { dispatch(connect()); }} text="Connect" />
|
||||||
|
<DefaultButton disabled={!selector.serialConnection.status.connected} onClick={() => { dispatch(disconnect()); }} text="Disconnect" />
|
||||||
|
</DialogFooter>
|
||||||
|
</Dialog>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Settings;
|
183
src/electron.js
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
/* eslint global-require: off, no-console: off */
|
||||||
|
const path = require("path");
|
||||||
|
const { BrowserWindow } = require("electron-acrylic-window");
|
||||||
|
|
||||||
|
const {
|
||||||
|
app, Tray, Menu, ipcMain,
|
||||||
|
} = require("electron");
|
||||||
|
const isDev = require("electron-is-dev");
|
||||||
|
const screenz = require("screenz");
|
||||||
|
const els = require("electron-localshortcut");
|
||||||
|
const { default: installExtension, REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS } = require("electron-devtools-installer");
|
||||||
|
|
||||||
|
const windowWidth = 900;
|
||||||
|
const windowHeight = 230;
|
||||||
|
|
||||||
|
let tray = null;
|
||||||
|
let mainWindow = null;
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
|
||||||
|
const extensions = [REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS];
|
||||||
|
|
||||||
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling
|
||||||
|
if (require("electron-squirrel-startup")) {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const RESOURCES_PATH = app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, "assets")
|
||||||
|
: path.join(__dirname, "../assets");
|
||||||
|
|
||||||
|
const getAssetPath = (...paths) => path.join(RESOURCES_PATH, ...paths);
|
||||||
|
|
||||||
|
function createWindow() {
|
||||||
|
// Create the browser window.
|
||||||
|
const win = new BrowserWindow({
|
||||||
|
width: windowWidth,
|
||||||
|
height: windowHeight,
|
||||||
|
x: screenz.width - windowWidth,
|
||||||
|
y: screenz.height - windowHeight,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: true,
|
||||||
|
enableRemoteModule: true,
|
||||||
|
contextIsolation: false,
|
||||||
|
},
|
||||||
|
transparent: true,
|
||||||
|
movable: false,
|
||||||
|
resizable: false,
|
||||||
|
frame: false,
|
||||||
|
show: false,
|
||||||
|
skipTaskbar: true,
|
||||||
|
alwaysOnTop: true,
|
||||||
|
vibrancy: {
|
||||||
|
effect: "acrylic",
|
||||||
|
useCustomWindowRefreshMethod: true,
|
||||||
|
disableOnBlur: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// and load the index.html of the app.
|
||||||
|
win.loadURL(
|
||||||
|
isDev
|
||||||
|
? "http://localhost:8420"
|
||||||
|
: `file://${path.join(__dirname, "../build/index.html")}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
win.webContents.openDevTools({ mode: "detach" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register shortcut to open devtools
|
||||||
|
els.register(win, "Ctrl+Shift+I", () => {
|
||||||
|
if (win.webContents.isDevToolsOpened()) {
|
||||||
|
win.webContents.closeDevTools();
|
||||||
|
} else {
|
||||||
|
win.webContents.openDevTools({ mode: "detach" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
|
// for applications and their menu bar to stay active until the user quits
|
||||||
|
// explicitly with Cmd + Q.
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("activate", () => {
|
||||||
|
// On macOS it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hide window when out of focus
|
||||||
|
app.on("browser-window-blur", () => {
|
||||||
|
if (!isDev) {
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// In this file you can include the rest of your app's specific main process
|
||||||
|
// code. You can also put them in separate files and require them here.
|
||||||
|
|
||||||
|
function createTray() {
|
||||||
|
const trayIcon = new Tray(getAssetPath("tray/tray-yellow-32.png"));
|
||||||
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: "Show",
|
||||||
|
click: () => {
|
||||||
|
mainWindow.show();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Settings",
|
||||||
|
click: () => {
|
||||||
|
mainWindow.webContents.send("show-settings", true);
|
||||||
|
mainWindow.show();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Exit",
|
||||||
|
click: () => {
|
||||||
|
app.quit();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
trayIcon.setToolTip("Heliox");
|
||||||
|
trayIcon.setContextMenu(contextMenu);
|
||||||
|
return trayIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force single app instance
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.quit();
|
||||||
|
} else {
|
||||||
|
app.on("second-instance", () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("ready", async () => {
|
||||||
|
app.allowRendererProcessReuse = false;
|
||||||
|
mainWindow = createWindow();
|
||||||
|
|
||||||
|
console.log("Installing extensions");
|
||||||
|
installExtension(extensions)
|
||||||
|
.then((name) => console.log(`Added Extension: ${name}`))
|
||||||
|
.catch((err) => console.log("An error occurred: ", err));
|
||||||
|
|
||||||
|
const ElectronStore = require("electron-store");
|
||||||
|
ElectronStore.initRenderer();
|
||||||
|
|
||||||
|
mainWindow.setMenu(null);
|
||||||
|
|
||||||
|
mainWindow.webContents.on("did-frame-finish-load", () => {
|
||||||
|
// Create tray icon with context menu
|
||||||
|
tray = createTray();
|
||||||
|
mainWindow.setBounds({ y: (screenz.height - mainWindow.getBounds().height) - tray.getBounds().height });
|
||||||
|
|
||||||
|
// Listen to tray icon onclick event
|
||||||
|
tray.on("click", () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.on("asynchronous-message", (event, arg) => {
|
||||||
|
if (arg === "green") {
|
||||||
|
tray.setImage(getAssetPath("tray/tray-green-32.png"));
|
||||||
|
} else if (arg === "red") {
|
||||||
|
tray.setImage(getAssetPath("tray/tray-red-32.png"));
|
||||||
|
} else if (arg === "yellow") {
|
||||||
|
tray.setImage(getAssetPath("tray/tray-yellow-32.png"));
|
||||||
|
} else if (arg === "default") {
|
||||||
|
tray.setImage(getAssetPath("tray/tray-default-32.png"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
11
src/index.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Heliox</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<div id="portal"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
16
src/index.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import React from "react";
|
||||||
|
import { render } from "react-dom";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { PersistGate } from "redux-persist/integration/react";
|
||||||
|
import store, { persistor } from "./redux/store";
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
|
<App />
|
||||||
|
</PersistGate>
|
||||||
|
</Provider>,
|
||||||
|
document.getElementById("root"),
|
||||||
|
);
|
10
src/interfaces/ISerialConnectionState.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export default interface ISerialConnectionState {
|
||||||
|
port: string;
|
||||||
|
message: string;
|
||||||
|
status: {
|
||||||
|
connecting: boolean;
|
||||||
|
connected: boolean;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line semi
|
||||||
|
}
|
6
src/redux/hooks/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||||
|
import { AppDispatch, RootState } from "../store/types.d";
|
||||||
|
|
||||||
|
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||||
|
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||||
|
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
10
src/redux/middlewares/logger.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Middleware } from "@reduxjs/toolkit";
|
||||||
|
import { RootState } from "../store/types.d";
|
||||||
|
|
||||||
|
const logger: Middleware<{}, RootState> = (store) => (next) => (action) => {
|
||||||
|
console.log(action.type);
|
||||||
|
console.log(store.getState());
|
||||||
|
return next(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default logger;
|
74
src/redux/middlewares/serialConnection.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { Middleware, Action, createAction } from "@reduxjs/toolkit";
|
||||||
|
import electron from "electron";
|
||||||
|
import { REHYDRATE } from "redux-persist";
|
||||||
|
import { RootState } from "../store/types.d";
|
||||||
|
import {
|
||||||
|
connect, disconnect, setSerialPort, setMessage, sendMessage, connectionStart, connectionFailure, connectionSuccess, connectionEnd,
|
||||||
|
} from "../slices/serialConnectionSlice";
|
||||||
|
import PortController from "../../serial/PortController";
|
||||||
|
|
||||||
|
let serialPort: PortController|null = null;
|
||||||
|
|
||||||
|
const serialConnection: Middleware<{}, RootState> = (state) => (next) => (action: Action) => {
|
||||||
|
const errorCalbackHandler = (error: Error | null | undefined) => {
|
||||||
|
if (error === null || error === undefined) {
|
||||||
|
state.dispatch(connectionSuccess());
|
||||||
|
} else {
|
||||||
|
state.dispatch(connectionFailure(error.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const closeCalbackHandler = (error: Error | null | undefined) => {
|
||||||
|
if (error !== null && error !== undefined) {
|
||||||
|
if (error.message === "Reading from COM port (ReadIOCompletion): Access denied") {
|
||||||
|
state.dispatch(connectionFailure(`Device ${state.getState().serialConnection.port} disconnected`));
|
||||||
|
state.dispatch(connectionEnd());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const serialDataListener = (msg: string) => {
|
||||||
|
if (msg !== state.getState().serialConnection.message) {
|
||||||
|
state.dispatch(setMessage(msg));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const connection = { serialConnection: state.getState().serialConnection };
|
||||||
|
const rehydrate = createAction < typeof connection >(REHYDRATE);
|
||||||
|
if (rehydrate.match(action)) {
|
||||||
|
state.dispatch(setSerialPort(action.payload.serialConnection.port));
|
||||||
|
state.dispatch(connect());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connect.match(action)) {
|
||||||
|
state.dispatch(connectionStart());
|
||||||
|
if (state.getState().serialConnection.port === "" || serialPort === null) {
|
||||||
|
state.dispatch(connectionFailure("No Serial Port set"));
|
||||||
|
return next(action);
|
||||||
|
}
|
||||||
|
if (serialPort.isOpen) {
|
||||||
|
state.dispatch(disconnect());
|
||||||
|
}
|
||||||
|
serialPort?.open(errorCalbackHandler, closeCalbackHandler);
|
||||||
|
serialPort?.parser.on("data", serialDataListener);
|
||||||
|
} else if (connectionSuccess.match(action)) {
|
||||||
|
electron.ipcRenderer.send("asynchronous-message", "default");
|
||||||
|
} else if (connectionFailure.match(action)) {
|
||||||
|
electron.ipcRenderer.send("asynchronous-message", "red");
|
||||||
|
const { Notification } = electron.remote;
|
||||||
|
new Notification({ title: "Connection Failure", body: action.payload }).show();
|
||||||
|
} else if (disconnect.match(action)) {
|
||||||
|
serialPort?.close();
|
||||||
|
state.dispatch(connectionEnd());
|
||||||
|
} else if (connectionEnd.match(action)) {
|
||||||
|
electron.ipcRenderer.send("asynchronous-message", "yellow");
|
||||||
|
} else if (setSerialPort.match(action)) {
|
||||||
|
if (serialPort?.isOpen) {
|
||||||
|
serialPort.close();
|
||||||
|
}
|
||||||
|
serialPort = new PortController(action.payload);
|
||||||
|
} else if (sendMessage.match(action)) {
|
||||||
|
serialPort?.write(action.payload);
|
||||||
|
}
|
||||||
|
return next(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default serialConnection;
|
62
src/redux/slices/serialConnectionSlice.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/* eslint-disable no-param-reassign */
|
||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import ISerialConnectionState from "../../interfaces/ISerialConnectionState";
|
||||||
|
// import type { RootState } from "../store/types.d";
|
||||||
|
|
||||||
|
// Define the initial state using that type
|
||||||
|
const initialState: ISerialConnectionState = {
|
||||||
|
port: "",
|
||||||
|
message: "0,0,0,0",
|
||||||
|
status: {
|
||||||
|
connecting: false,
|
||||||
|
connected: false,
|
||||||
|
error: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serialConnectionSlice = createSlice({
|
||||||
|
name: "serialConnection",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setSerialPort: (state, action: PayloadAction<string>) => {
|
||||||
|
state.port = action.payload;
|
||||||
|
},
|
||||||
|
connectionStart: (state) => {
|
||||||
|
state.status.connecting = true;
|
||||||
|
state.status.connected = false;
|
||||||
|
state.status.error = "";
|
||||||
|
},
|
||||||
|
connectionSuccess: (state) => {
|
||||||
|
state.status.connecting = false;
|
||||||
|
state.status.connected = true;
|
||||||
|
state.status.error = "";
|
||||||
|
},
|
||||||
|
connectionFailure: (state, action: PayloadAction<string>) => {
|
||||||
|
state.status.connecting = false;
|
||||||
|
state.status.connected = false;
|
||||||
|
state.status.error = action.payload;
|
||||||
|
},
|
||||||
|
connectionEnd: (state) => {
|
||||||
|
state.status.connecting = false;
|
||||||
|
state.status.connected = false;
|
||||||
|
},
|
||||||
|
connect: () => {},
|
||||||
|
disconnect: () => {},
|
||||||
|
setMessage: (state, action: PayloadAction<string>) => {
|
||||||
|
state.message = action.payload;
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
sendMessage: (_state, _action: PayloadAction<string>) => {
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setSerialPort, connectionStart, connectionSuccess, connectionFailure, connectionEnd,
|
||||||
|
connect, disconnect, setMessage, sendMessage,
|
||||||
|
} = serialConnectionSlice.actions;
|
||||||
|
|
||||||
|
// Other code such as selectors can use the imported `RootState` type
|
||||||
|
// export const selectCount = (state: RootState) => state.counter.value;
|
||||||
|
|
||||||
|
export default serialConnectionSlice.reducer;
|
48
src/redux/store/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import {
|
||||||
|
persistStore,
|
||||||
|
persistReducer,
|
||||||
|
FLUSH,
|
||||||
|
REHYDRATE,
|
||||||
|
PAUSE,
|
||||||
|
PERSIST,
|
||||||
|
PURGE,
|
||||||
|
REGISTER,
|
||||||
|
} from "redux-persist";
|
||||||
|
import createElectronStorage from "redux-persist-electron-storage";
|
||||||
|
import ElectronStore from "electron-store";
|
||||||
|
import serialConnection from "../middlewares/serialConnection";
|
||||||
|
import rootReducer from "./root-reducer";
|
||||||
|
|
||||||
|
export const electronStore = new ElectronStore();
|
||||||
|
|
||||||
|
createElectronStorage({
|
||||||
|
electronStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
const persistConfig = {
|
||||||
|
key: "serialConnection",
|
||||||
|
storage: createElectronStorage({
|
||||||
|
electronStore,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||||
|
|
||||||
|
const middlewares = [serialConnection];
|
||||||
|
|
||||||
|
const store = configureStore(
|
||||||
|
{
|
||||||
|
reducer: persistedReducer,
|
||||||
|
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
|
||||||
|
serializableCheck: {
|
||||||
|
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
|
||||||
|
},
|
||||||
|
}).concat(middlewares),
|
||||||
|
devTools: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default store;
|
||||||
|
|
||||||
|
export const persistor = persistStore(store);
|
8
src/redux/store/root-reducer.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { combineReducers } from "redux";
|
||||||
|
import SerialConnectionReducer from "../slices/serialConnectionSlice";
|
||||||
|
|
||||||
|
const rootReducer = combineReducers({
|
||||||
|
serialConnection: SerialConnectionReducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rootReducer;
|
5
src/redux/store/types.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import store from ".";
|
||||||
|
import rootReducer from "./root-reducer";
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof rootReducer>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
55
src/serial/PortController.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import SerialPort, { parsers } from "serialport";
|
||||||
|
|
||||||
|
class PortController {
|
||||||
|
path: string;
|
||||||
|
|
||||||
|
port: SerialPort;
|
||||||
|
|
||||||
|
parser: any;
|
||||||
|
|
||||||
|
onCloseCallback: (error:Error)=>void;
|
||||||
|
|
||||||
|
constructor(path: string) {
|
||||||
|
this.path = path;
|
||||||
|
this.port = new SerialPort(path, {
|
||||||
|
baudRate: 9600,
|
||||||
|
autoOpen: false,
|
||||||
|
});
|
||||||
|
this.parser = this.port.pipe(new parsers.Readline({ delimiter: "\r\n" }));
|
||||||
|
this.onCloseCallback = () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get isOpen(): boolean {
|
||||||
|
if (this.port == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.port.isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
get getParser(): any {
|
||||||
|
return this.parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(callback: (error:Error | null | undefined)=>void, onCloseCallback:(error:Error | null | undefined)=>void) {
|
||||||
|
if (this.isOpen) {
|
||||||
|
throw new Error("Port already open");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.port.open((error) => callback(error));
|
||||||
|
this.port.on("close", onCloseCallback);
|
||||||
|
this.onCloseCallback = onCloseCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this.port) {
|
||||||
|
this.port.close();
|
||||||
|
this.port.removeListener("data", this.onCloseCallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write(message: string) {
|
||||||
|
this.port.write(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PortController;
|
14
styled.d.ts
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// import original module declarations
|
||||||
|
import "styled-components";
|
||||||
|
|
||||||
|
// and extend them!
|
||||||
|
declare module "styled-components" {
|
||||||
|
export interface DefaultTheme {
|
||||||
|
borderRadius: string;
|
||||||
|
|
||||||
|
colors: {
|
||||||
|
main: string;
|
||||||
|
secondary: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
24
tsconfig.json
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2018",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"lib": ["dom", "esnext"],
|
||||||
|
"declaration": false,
|
||||||
|
"noEmit": false,
|
||||||
|
"jsx": "react",
|
||||||
|
"strict": true,
|
||||||
|
"pretty": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowJs": true
|
||||||
|
},
|
||||||
|
"include": ["src", "assets/assets.d.ts"],
|
||||||
|
"exclude": ["src/electron.js"]
|
||||||
|
}
|
79
webpack.config.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
|
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
|
||||||
|
|
||||||
|
module.exports = (env, argv) => {
|
||||||
|
const nodeModules = { };
|
||||||
|
fs.readdirSync("node_modules")
|
||||||
|
.filter((x) => [".bin"].indexOf(x) === -1)
|
||||||
|
.forEach((mod) => {
|
||||||
|
nodeModules[mod] = `commonjs ${mod}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
entry: "./src/index.tsx",
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, "build"),
|
||||||
|
filename: "index.bundle.js",
|
||||||
|
},
|
||||||
|
mode: process.env.NODE_ENV || "development",
|
||||||
|
resolve: {
|
||||||
|
extensions: [".tsx", ".ts", ".js"],
|
||||||
|
},
|
||||||
|
devtool: "source-map",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: ["babel-loader"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(ts|tsx)$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: "ts-loader",
|
||||||
|
options: {
|
||||||
|
transpileOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(css|scss)$/,
|
||||||
|
use: ["style-loader", "css-loader"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|jp(e*)g|svg|gif)$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: "file-loader",
|
||||||
|
options: {
|
||||||
|
name: "assets/[hash]-[name].[ext]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: path.join(__dirname, "src", "index.html"),
|
||||||
|
}),
|
||||||
|
new ForkTsCheckerWebpackPlugin(),
|
||||||
|
{
|
||||||
|
apply: (compiler) => {
|
||||||
|
compiler.hooks.done.tap("DonePlugin", () => {
|
||||||
|
if (argv.mode === "production") {
|
||||||
|
setTimeout(() => {
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
target: "electron-renderer",
|
||||||
|
externals: nodeModules,
|
||||||
|
};
|
||||||
|
};
|