Initial commit

This commit is contained in:
2025-11-17 08:44:00 +01:00
parent 5ef0bca588
commit 6b384d43ff
46 changed files with 7876 additions and 0 deletions

4
frontend/.browserslistrc Normal file
View File

@@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

5
frontend/.editorconfig Normal file
View File

@@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

22
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,22 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

79
frontend/README.md Normal file
View File

@@ -0,0 +1,79 @@
# Vuetify (Default)
This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch.
## ❗️ Important Links
- 📄 [Docs](https://vuetifyjs.com/)
- 🚨 [Issues](https://issues.vuetifyjs.com/)
- 🏬 [Store](https://store.vuetifyjs.com/)
- 🎮 [Playground](https://play.vuetifyjs.com/)
- 💬 [Discord](https://community.vuetifyjs.com)
## 💿 Install
Set up your project using your preferred package manager. Use the corresponding command to install the dependencies:
| Package Manager | Command |
|---------------------------------------------------------------|----------------|
| [yarn](https://yarnpkg.com/getting-started) | `yarn install` |
| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` |
| [pnpm](https://pnpm.io/installation) | `pnpm install` |
| [bun](https://bun.sh/#getting-started) | `bun install` |
After completing the installation, your environment is ready for Vuetify development.
## ✨ Features
- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/)
- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue.
- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts)
-**Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/)
- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components)
These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable.
## 💡 Usage
This section covers how to start the development server and build your project for production.
### Starting the Development Server
To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000):
```bash
yarn dev
```
(Repeat for npm, pnpm, and bun with respective commands.)
> Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script.
### Building for Production
To build your project for production, use:
```bash
yarn build
```
(Repeat for npm, pnpm, and bun with respective commands.)
Once the build process is completed, your application will be ready for deployment in a production environment.
## 💪 Support Vuetify Development
This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider:
- [Requesting Enterprise Support](https://support.vuetifyjs.com/)
- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship)
- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship)
- [Supporting the team on Open Collective](https://opencollective.com/vuetify)
- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify)
- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify)
- [Making a one-time donation with Paypal](https://paypal.me/vuetify)
## 📑 License
[MIT](http://opensource.org/licenses/MIT)
Copyright (c) 2016-present Vuetify, LLC

25
frontend/components.d.ts vendored Normal file
View File

@@ -0,0 +1,25 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AppHeader: typeof import('./src/components/AppHeader.vue')['default']
Atmo: typeof import('./src/components/Atmo.vue')['default']
ControlButton: typeof import('./src/components/BaseComponents/ControlButton.vue')['default']
Gasflow: typeof import('./src/components/Gasflow.vue')['default']
ManualMode: typeof import('./src/components/ManualMode.vue')['default']
MotorControl: typeof import('./src/components/BaseComponents/MotorControl.vue')['default']
ProcessValue: typeof import('./src/components/BaseComponents/ProcessValue.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ValveControl: typeof import('./src/components/BaseComponents/ValveControl.vue')['default']
}
}

View File

@@ -0,0 +1,3 @@
//import vuetify from 'eslint-config-vuetify'
//export default vuetify()

13
frontend/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to Vuetify 3</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

20
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"allowJs": true,
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "bundler",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}

5722
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
frontend/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "frontend_v2",
"private": true,
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --fix"
},
"dependencies": {
"@fontsource/roboto": "5.2.7",
"@mdi/font": "7.4.47",
"@ygoe/msgpack": "^1.0.3",
"pinia": "^3.0.4",
"vue": "^3.5.21",
"vue-router": "^4.5.1",
"vuetify": "^3.10.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"eslint": "^9.35.0",
"eslint-config-vuetify": "^4.2.0",
"sass-embedded": "^1.92.1",
"unplugin-fonts": "^1.4.0",
"unplugin-vue-components": "^29.0.0",
"unplugin-vue-router": "^0.15.0",
"vite": "^7.1.5",
"vite-plugin-vuetify": "^2.1.2"
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

27
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,27 @@
<template>
<v-app>
<app-header />
<v-main>
<router-view />
</v-main>
</v-app>
</template>
<script>
import { mapStores } from 'pinia'
import AppHeader from '@/components/AppHeader.vue'
import { useOpcuaStore } from '@/stores/opcua'
export default {
name: 'App',
components: {
AppHeader,
},
computed: {
...mapStores(useOpcuaStore),
},
mounted() {
this.opcuaStore.connect()
},
};
</script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,6 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
</svg>

After

Width:  |  Height:  |  Size: 526 B

View File

@@ -0,0 +1,36 @@
<template>
<v-app-bar title="Nikon SLM - Polaris">
<control-button nodeId="ns=4;s=GVL_SCADA.stConfirmAlarms">
Ack Alarms
</control-button>
<!-- <v-btn @click.prevent="changeTheme" icon="mdi-theme-light-dark"></v-btn> -->
<v-app-bar-nav-icon><v-icon>mdi-account</v-icon></v-app-bar-nav-icon>
</v-app-bar>
<v-navigation-drawer permanent>
<v-list density="compact" nav>
<v-list-item prepend-icon="mdi-home-city" title="Home" value="home" to="/"></v-list-item>
<v-list-item prepend-icon="mdi-hand-back-left" title="Manual" value="manual"></v-list-item>
<v-list-item prepend-icon="mdi-bell" title="Alerts" value="alerts"></v-list-item>
<v-list-item prepend-icon="mdi-cog" title="Settings" value="settings"></v-list-item>
</v-list>
</v-navigation-drawer>
</template>
<script>
import ControlButton from '@/components/BaseComponents/ControlButton.vue'
export default {
name: 'AppHeader',
data() {
return {}
},
components: {
ControlButton,
},
// methods: {
// changeTheme() {
// this.$vuetify.theme.toggle()
// },
// },
}
</script>

View File

@@ -0,0 +1,53 @@
<template>
<v-sheet
class="d-flex flex-column pa-4 ga-4"
border="solid md"
rounded="lg"
width="300"
>
<v-btn :to="detailsLink" variant="flat" size="x-large">Atmosphere</v-btn>
<v-divider></v-divider>
<v-sheet class="d-flex flex-column">
<v-row>
<v-col cols="6"><control-button :nodeId="atStartButton" block>Start</control-button></v-col>
<v-col cols="6"><control-button :nodeId="atStopButton" block>Stop</control-button></v-col>
</v-row>
</v-sheet>
</v-sheet>
</template>
<script>
import { mapStores } from 'pinia'
import { useOpcuaStore } from '@/stores/opcua'
import ControlButton from './BaseComponents/ControlButton.vue'
export default {
name: 'Atmocontrol',
props: {
nodeId: {
type: String,
required: true,
},
detailsLink: {
type: String
},
},
computed: {
...mapStores(useOpcuaStore),
atStartButton() {
return `${this.nodeId}.stOpenAllValvesButton`
},
atStopButton() {
return `${this.nodeId}.stCloseAllValvesButton`
},
},
components: {
ControlButton,
},
methods: {
mounted() {
this.opcuaStore.subscribe([this.atStartButton, this.atStopButton])
}
},
}
</script>

View File

@@ -0,0 +1,51 @@
<template>
<v-btn
:disabled="!this.opcuaStore.values[this.cbReleaseNodeId]"
:class="{ 'bg-primary': isActive }"
@click="onClick"
>
<slot></slot>
</v-btn>
</template>
<script>
import { mapStores } from 'pinia'
import { useOpcuaStore } from '@/stores/opcua'
export default {
name: 'ControlButton',
computed: {
...mapStores(useOpcuaStore),
cbFeedbackNodeId() {
return `${this.nodeId}.eFeedback`
},
cbReleaseNodeId() {
return `${this.nodeId}.xRelease`
},
cbRequestNodeId() {
return `${this.nodeId}.xRequest`
},
isActive() {
return this.opcuaStore.values[this.cbFeedbackNodeId]
},
},
props: {
nodeId: {
type: String,
required: true,
},
},
methods: {
onClick() {
this.opcuaStore.sendCommand({
nodeId: this.cbRequestNodeId,
nodeValue: true,
nodeType: this.opcuaStore.opcuaDatatypes.Boolean,
})
},
},
mounted() {
this.opcuaStore.subscribe([this.cbFeedbackNodeId, this.cbReleaseNodeId])
},
}
</script>

View File

@@ -0,0 +1,149 @@
<template>
<v-card :title="motorName" max-width="300" w-auto>
<v-card-text>
<v-row justify="center">
<v-col cols="6">
<v-text-field
v-model="choosenSetpoint"
:rules="checkSetpoint"
:reverse="true"
:disabled="opcuaStore.values[currentControlModeNodeId] != 2"
:prefix="opcuaStore.values[spUnitNodeId]"
@focus="$event.target.select()"
@keyup.enter="onSetpointEnter"
label="Setpoint"
></v-text-field>
</v-col>
<v-col cols="6">
<h1>/ {{ processValue }}</h1>
</v-col>
</v-row>
<v-row>
<v-col cols="6">
<control-button block :node-id="automaticModeCBNodeId">
Auto
</control-button>
</v-col>
<v-col cosl="6">
<control-button block :node-id="manualModeCBNodeId">
Manual
</control-button>
</v-col>
</v-row>
<v-row>
<v-col cols="6">
<control-button block :node-id="startCBNodeId">
Start
</control-button>
</v-col>
<v-col cosl="6">
<control-button block :node-id="stopCBNodeId"> Stop </control-button>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
import { mapStores } from 'pinia'
import { useOpcuaStore } from '@/stores/opcua'
import ControlButton from './ControlButton.vue'
export default {
name: 'MotorControl',
data() {
return {
choosenSetpoint: 0,
checkSetpoint: [
(value) => {
if (
value <= this.opcuaStore.values[this.spMaxValueNodeId] &&
value >= this.opcuaStore.values[this.spMinValueNodeId]
) {
return true
} else {
return `Setpoint must be <= ${this.opcuaStore.values[this.spMaxValueNodeId]} and >= ${this.opcuaStore.values[this.spMinValueNodeId]}`
}
},
],
}
},
computed: {
...mapStores(useOpcuaStore),
automaticModeCBNodeId() {
return `${this.nodeId}.stAutomaticButton`
},
manualModeCBNodeId() {
return `${this.nodeId}.stManualButton`
},
startCBNodeId() {
return `${this.nodeId}.stStartButton`
},
stopCBNodeId() {
return `${this.nodeId}.stStopButton`
},
spNodeId() {
return `${this.nodeId}.stSetpoint.rValue`
},
currentControlModeNodeId() {
return `${this.nodeId}.iCurrentMode`
},
spMinValueNodeId() {
return `${this.nodeId}.stSetpoint.rMin`
},
spMaxValueNodeId() {
return `${this.nodeId}.stSetpoint.rMax`
},
spUnitNodeId() {
return `${this.nodeId}.stSetpoint.sUnit`
},
pvUnitNodeId() {
return `${this.nodeId}.stProcessValue.sUnit`
},
pvNodeId() {
return `${this.nodeId}.stProcessValue.rValue`
},
processValue() {
const value = Number(this.opcuaStore.values[this.pvNodeId]).toFixed(this.numDecimals)
const unit = this.opcuaStore.values[this.pvUnitNodeId]
return `${value} ${unit}`
},
},
components: {
ControlButton,
},
props: {
nodeId: {
type: String,
required: true,
},
motorName: {
type: String,
},
numDecimals: {
type: Number,
default: 1,
},
},
methods: {
onSetpointEnter(event) {
event.target.blur()
this.opcuaStore.sendCommand({
nodeId: this.spNodeId,
nodeValue: this.choosenSetpoint,
nodeType: this.opcuaStore.opcuaDatatypes.Float,
})
},
},
mounted() {
this.opcuaStore.subscribe([
this.currentControlModeNodeId,
this.spMinValueNodeId,
this.spMaxValueNodeId,
this.spUnitNodeId,
this.pvUnitNodeId,
this.pvNodeId,
])
},
}
</script>

View File

@@ -0,0 +1,43 @@
<template>
<p class="text-right">{{ formattedString }}</p>
</template>
<script>
import { mapStores } from 'pinia'
import { useOpcuaStore } from '@/stores/opcua'
export default {
name: 'ProcessValue',
computed: {
...mapStores(useOpcuaStore),
formattedString() {
const value = Number(this.opcuaStore.values[this.pvValueNModeId]).toFixed(this.numDecimals)
const unit = this.opcuaStore.values[this.pvUnitNModeId]
return `${value} ${unit}`
},
pvValueNModeId() {
return `${this.nodeId}.rValue`
},
pvUnitNModeId() {
return `${this.nodeId}.sUnit`
},
},
props: {
numDecimals: {
type: Number,
default: 1,
},
nodeId: {
type: String,
required: true,
},
},
mounted() {
this.opcuaStore.subscribe([this.pvValueNModeId, this.pvUnitNModeId])
},
// unmounted() {
// console.log("Unsubscribed");
// this.opcuaStore.unsubscribe([this.nodeIdValue, this.nodeIdUnit]);
// },
}
</script>

View File

@@ -0,0 +1,65 @@
<template>
<v-card :title="valveName">
<v-card-text>
<v-row>
<v-col cols="6">
<control-button block :nodeId="automaticModeCBNodeId">
Auto
</control-button>
</v-col>
<v-col cosl="6">
<control-button block :nodeId="manualModeCBNodeId">
Manual
</control-button>
</v-col>
</v-row>
<v-row>
<v-col cols="6">
<control-button block :nodeId="openCBNodeId"> Open </control-button>
</v-col>
<v-col>
<control-button block :nodeId="closeCBNodeId">
Close
</control-button>
</v-col>
</v-row>
</v-card-text>
</v-card>
</template>
<script>
import { mapStores } from 'pinia'
import { useOpcuaStore } from '@/stores/opcua'
import ControlButton from './ControlButton.vue'
export default {
name: 'ValveControl',
computed: {
...mapStores(useOpcuaStore),
automaticModeCBNodeId() {
return `${this.nodeId}.stAutomaticButton`
},
manualModeCBNodeId() {
return `${this.nodeId}.stManualButton`
},
openCBNodeId() {
return `${this.nodeId}.stOpenButton`
},
closeCBNodeId() {
return `${this.nodeId}.stCloseButton`
},
},
components: {
ControlButton,
},
props: {
nodeId: {
type: String,
required: true,
},
valveName: {
type: String,
},
},
}
</script>

View File

@@ -0,0 +1,82 @@
<template>
<v-sheet
class="d-flex flex-column pa-4 ga-4"
border="solid md"
rounded="lg"
width="300"
>
<v-btn :to="detailsLink" variant="flat" size="x-large">Gasflow</v-btn>
<v-divider></v-divider>
<v-sheet class="d-flex flex-column">
<v-row>
<v-col cols="6"><h4 class="text-right">Flow:</h4></v-col>
<v-col cols="6"><process-value :nodeId="gfFlowNodeId"/></v-col>
</v-row>
<v-row>
<v-col cols="6"><h4 class="text-right">Temp:</h4></v-col>
<v-col cols="6"><process-value :nodeId="gfTempNodeId"/></v-col>
</v-row>
</v-sheet>
<v-divider></v-divider>
<v-sheet class="d-flex flex-column">
<v-row>
<v-col cols="6"><control-button :nodeId="gfAutoButton" block>Automatic</control-button></v-col>
<v-col cols="6"><control-button :nodeId="gfManualButton" block>Manual</control-button></v-col>
</v-row>
<v-row>
<v-col cols="6"><control-button :nodeId="gfStartButton" block>Start</control-button></v-col>
<v-col cols="6"><control-button :nodeId="gfStopButton" block>Stop</control-button></v-col>
</v-row>
</v-sheet>
</v-sheet>
</template>
<script>
import { mapStores } from 'pinia'
import { useOpcuaStore } from '@/stores/opcua'
import ControlButton from './BaseComponents/ControlButton.vue'
import ProcessValue from './BaseComponents/ProcessValue.vue'
export default {
name: 'Gasflow',
props: {
nodeId: {
type: String,
required: true,
},
detailsLink: {
type: String
},
},
computed: {
...mapStores(useOpcuaStore),
gfFlowNodeId() {
return `${this.nodeId}.stFlowrateSensor`
},
gfTempNodeId() {
return `${this.nodeId}.stTempSensor`
},
gfAutoButton() {
return `${this.nodeId}.stAutomaticButton`
},
gfManualButton() {
return `${this.nodeId}.stManualButton`
},
gfStartButton() {
return `${this.nodeId}.stStartButton`
},
gfStopButton() {
return `${this.nodeId}.stStopButton`
},
},
components: {
ControlButton,
ProcessValue,
},
methods: {
mounted() {
this.opcuaStore.subscribe([this.gfFlowNodeId, this.gfTempNodeId, this.gfAutoButton, this.gfManualButton, this.gfStartButton, this.gfStopButton])
}
},
}
</script>

26
frontend/src/main.js Normal file
View File

@@ -0,0 +1,26 @@
/**
* main.js
*
* Bootstraps Vuetify and other plugins then mounts the App`
*/
// Plugins
import { registerPlugins } from '@/plugins'
// Components
import App from './App.vue'
// Composables
import { createApp } from 'vue'
import { createPinia } from 'pinia'
// Styles
import 'unfonts.css'
const pinia = createPinia()
const app = createApp(App)
registerPlugins(app)
app.use(pinia)
app.mount('#app')

View File

@@ -0,0 +1,3 @@
# Plugins
Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally.

View File

@@ -0,0 +1,15 @@
/**
* plugins/index.js
*
* Automatically included in `./src/main.js`
*/
// Plugins
import vuetify from './vuetify'
import router from '@/router'
export function registerPlugins (app) {
app
.use(vuetify)
.use(router)
}

View File

@@ -0,0 +1,19 @@
/**
* plugins/vuetify.js
*
* Framework documentation: https://vuetifyjs.com`
*/
// Styles
import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles'
// Composables
import { createVuetify } from 'vuetify'
// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides
export default createVuetify({
theme: {
defaultTheme: 'system',
},
})

View File

@@ -0,0 +1,55 @@
/**
* router/index.ts
*
* Automatic routes for `./src/pages/*.vue`
*/
// Composables
import { createRouter, createWebHistory } from 'vue-router'
// import { routes } from 'vue-router/auto-routes'
import Home from '@/views/HomeView.vue'
//import { components } from 'vuetify/dist/vuetify.js'
import AtmosphereManualView from '@/views/AtmosphereManualView.vue'
import GasflowManualView from '@/views/GasflowManualView.vue'
const routes = [
{
path: '/',
component: Home,
},
{
path: '/manual/gf',
component: GasflowManualView,
},
{
path: '/manual/atmo',
component: AtmosphereManualView,
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})
// Workaround for https://github.com/vitejs/vite/issues/11804
router.onError((err, to) => {
if (err?.message?.includes?.('Failed to fetch dynamically imported module')) {
if (localStorage.getItem('vuetify:dynamic-reload')) {
console.error('Dynamic import error, reloading page did not fix it', err)
} else {
console.log('Reloading page to fix dynamic import error')
localStorage.setItem('vuetify:dynamic-reload', 'true')
location.assign(to.fullPath)
}
} else {
console.error(err)
}
})
router.isReady().then(() => {
localStorage.removeItem('vuetify:dynamic-reload')
})
export default router

View File

@@ -0,0 +1,169 @@
import { defineStore } from "pinia";
import msgpack from "@ygoe/msgpack";
export const useOpcuaStore = defineStore("opcua", {
state: () => ({
values: {},
opcuaEvents: new Map(),
ws: null,
connected: false,
subscriptions: new Set(),
opcuaDatatypes: {
Null: 0,
Boolean: 1,
SByte: 2,
Byte: 3,
Int16: 4,
UInt16: 5,
Int32: 6,
UInt32: 7,
Int64: 8,
UInt64: 9,
Float: 10,
Double: 11,
String: 12,
DateTime: 13,
Guid: 14,
ByteString: 15,
XmlElement: 16,
NodeId: 17,
ExpandedNodeId: 18,
StatusCode: 19,
QualifiedName: 20,
LocalizedText: 21,
ExtensionObject: 22,
DataValue: 23,
Variant: 24,
DiagnosticInfo: 25,
},
reconnectTimeout: null,
}),
actions: {
connect() {
this.ws = new WebSocket("ws://127.0.0.1:8000/ws");
this.ws.binaryType = "arraybuffer";
this.ws.onopen = () => {
this.connected = true;
// Initiale Subscriptions senden
const msg = msgpack.encode({
subscribe: Array.from(this.subscriptions),
});
this.ws.send(msg);
};
this.ws.onmessage = (event) => {
const data = msgpack.decode(new Uint8Array(event.data));
if (data.initial) {
Object.entries(data.initial.values).forEach(([tag, val]) => {
this.values[tag] = val;
});
Object.entries(data.initial.events).forEach(([tag, val]) => {
this.opcuaEvents.set(tag, val);
});
}
if (data.u) {
Object.entries(data.u).forEach(([nodeId, val]) => {
this.values[nodeId] = val;
});
}
if (data.e) {
console.log(data.e);
Object.entries(data.e).forEach(([nodeId, event]) => {
// Event is still relevant
if (event.Retain) {
let entry = {};
// If we already have an event with the same nodeId
if (this.opcuaEvents.has(nodeId)) {
entry = this.opcuaEventsget(nodeId);
entry.type = event.Severity;
//entry.message = event.message;
// check if it is still active
if (event.ActiveState) {
entry.raised = new Date(event.ActiveStateTransTime).toLocaleString("de-DE");
} else {
// If not, write the time to cleared
entry.cleared = new Date(event.ActiveStateTransTime).toLocaleString("de-DE");
}
if (event.ConfirmedState) {
entry.confirmed = new Date(event.ConfirmedStateTransTime).toLocaleString("de-DE");
}
} else {
entry = {
type: event.Severity,
message: event.Message,
raised: new Date(event.ActiveStateTransTime).toLocaleString("de-DE"),
cleared: event.ActiveState ? "" : new Date(event.ActiveStateTransTime).toLocaleString("de-DE"),
confirmed: event.ConfirmedState ? new Date(event.ConfirmedStateTransTime).toLocaleString("de-DE") : "",
};
}
this.opcuaEvents.set(nodeId, entry);
} else {
this.opcuaEvents.delete(nodeId);
}
});
Object.entries(data.e).forEach(([nodeId, val]) => {
// Event is not yet cleared
// if (val.Retain)
// {
// console.log(val)
// //this.opcuaEvents.set(nodeId) = val;
// }
// else
// {
// if (this.opcuaEvents.has(nodeId)) {
// this.opcuaEvents.delete(nodeId);
// }
// }
});
}
};
this.ws.onclose = () => {
//this.reconnect();
this.connected = false;
};
},
reconnect() {
if (this.reconnectTimeout) return; // Verhindert mehrfachen Reconnect
this.reconnectTimeout = setTimeout(() => {
this.reconnectTimeout = null;
this.connect();
}, 3000);
},
subscribe(tags) {
tags.forEach((tag) => this.subscriptions.add(tag));
if (this.connected && this.ws) {
const msg = msgpack.encode({ subscribe: tags });
this.ws.send(msg);
}
},
unsubscribe(tags) {
tags.forEach((tag) => this.subscriptions.delete(tag));
if (this.connected && this.ws) {
const msg = msgpack.encode({ unsubscribe: tags });
this.ws.send(msg);
}
},
sendCommand(command) {
if (this.ws && this.connected) {
const msg = msgpack.encode({ command: command });
this.ws.send(msg);
}
},
getValue(tag) {
return this.values[tag];
},
},
});

View File

@@ -0,0 +1,3 @@
# Styles
This directory is for configuring the styles of the application.

View File

@@ -0,0 +1,10 @@
/**
* src/styles/settings.scss
*
* Configures SASS variables and Vuetify overwrites
*/
// https://vuetifyjs.com/features/sass-variables/`
// @use 'vuetify/settings' with (
// $color-pack: false
// );

View File

@@ -0,0 +1,42 @@
<template>
<v-container class="d-flex">
<v-row>
<v-col cols="3">
<valve-control
valve-name="QM01"
node-id="ns=4;s=GVL_SCADA.stPolarisHMIInterface.stAtmoTemp.stBeforePCValve"
/>
</v-col>
<v-col cols="3">
<valve-control
valve-name="QM02"
node-id="ns=4;s=GVL_SCADA.stPolarisHMIInterface.stAtmoTemp.stBeforePfmValveHMI"
/>
</v-col>
<v-col cols="3">
<valve-control
valve-name="QM09"
node-id="ns=4;s=GVL_SCADA.stPolarisHMIInterface.stAtmoTemp.stAfterPfmValveHMI"
/>
</v-col>
<v-col cols="3">
<valve-control
valve-name="QM40"
node-id="ns=4;s=GVL_SCADA.stPolarisHMIInterface.stAtmoTemp.stInertAndPurgeOutletValve"
/>
</v-col>
</v-row>
</v-container>
</template>
<script>
import ValveControl from '@/components/BaseComponents/ValveControl.vue'
export default {
name: 'AtmoManualView',
components: {
ValveControl,
}
}
</script>

View File

@@ -0,0 +1,30 @@
<template>
<v-container class="d-flex ga-6">
<v-row>
<v-col cols="3">
<v-sheet class="d-flex flex-column">
<v-row>
<v-col cols="6"><h4 class="text-right">Flow:</h4></v-col>
<v-col cols="6"><process-value nodeId="ns=4;s=GVL_SCADA.stPolarisHMIInterface.stGasFlow.stFlowrateSensor"/></v-col>
</v-row>
</v-sheet>
</v-col>
<v-col cols="3">
<motor-control nodeId="ns=4;s=GVL_SCADA.stPolarisHMIInterface.stGasFlow.stHMIBlower"/>
</v-col>
</v-row>
</v-container>
</template>
<script>
import ValveControl from '@/components/BaseComponents/ValveControl.vue'
import MotorControl from '@/components/BaseComponents/MotorControl.vue'
export default {
name: 'GFManualView',
components: {
MotorControl,
ValveControl,
},
}
</script>

View File

@@ -0,0 +1,22 @@
<template>
<v-container class="d-flex ga-6">
<gasflow detailsLink="/manual/gf" nodeId="ns=4;s=GVL_SCADA.stPolarisHMIInterface.stGasFlow" />
<atmo detailsLink="/manual/atmo" nodeId="ns=4;s=GVL_SCADA.stPolarisHMIInterface.stAtmoTemp" />
</v-container>
</template>
<script>
import Gasflow from '@/components/Gasflow.vue';
import Atmo from '@/components/Atmo.vue';
export default {
name: 'ManualView',
components: {
Gasflow,
Atmo,
},
}
</script>

46
frontend/typed-router.d.ts vendored Normal file
View File

@@ -0,0 +1,46 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
declare module 'vue-router/auto-routes' {
import type {
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'vue-router'
/**
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
}
/**
* Route file to route info map by unplugin-vue-router.
* Used by the volar plugin to automatically type useRoute()
*
* Each key is a file path relative to the project root with 2 properties:
* - routes: union of route names of the possible routes when in this page (passed to useRoute<...>())
* - views: names of nested views (can be passed to <RouterView name="...">)
*
* @internal
*/
export interface _RouteFileInfoMap {
}
/**
* Get a union of possible route names in a certain route component file.
* Used by the volar plugin to automatically type useRoute()
*
* @internal
*/
export type _RouteNamesForFilePath<FilePath extends string> =
_RouteFileInfoMap extends Record<FilePath, infer Info>
? Info['routes']
: keyof RouteNamedMap
}

66
frontend/vite.config.mjs Normal file
View File

@@ -0,0 +1,66 @@
// Plugins
import Components from 'unplugin-vue-components/vite'
import Vue from '@vitejs/plugin-vue'
import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
import Fonts from 'unplugin-fonts/vite'
import VueRouter from 'unplugin-vue-router/vite'
// Utilities
import { defineConfig } from 'vite'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
VueRouter(),
Vue({
template: { transformAssetUrls },
}),
// https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme
Vuetify({
autoImport: true,
styles: {
configFile: 'src/styles/settings.scss',
},
}),
Components(),
Fonts({
fontsource: {
families: [
{
name: 'Roboto',
weights: [100, 300, 400, 500, 700, 900],
styles: ['normal', 'italic'],
},
],
},
}),
],
optimizeDeps: {
exclude: [
'vuetify',
'vue-router',
'unplugin-vue-router/runtime',
'unplugin-vue-router/data-loaders',
'unplugin-vue-router/data-loaders/basic',
],
},
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('src', import.meta.url)),
},
extensions: [
'.js',
'.json',
'.jsx',
'.mjs',
'.ts',
'.tsx',
'.vue',
],
},
server: {
port: 3000,
},
})