Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,9 @@ server/_appdata
#client/package-lock.json

server/_widgets

client/dist/**/*
app/electron/dist/**/*



562 changes: 562 additions & 0 deletions FUXA_EPICS_Paper.md

Large diffs are not rendered by default.

57 changes: 57 additions & 0 deletions LANGUAGE_OPTIMIZATION_GUIDE_ZH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# FUXA 语言设置优化与修复说明

本文档详细说明了针对 FUXA 系统设置中语言切换功能的优化内容及技术原理,旨在解决语言设置不生效及实时性差的问题。

---

## 1. 修复背景
在之前的版本中,用户在“系统配置 (Setup)”->“应用设置”界面修改语言时,存在以下问题:
* **切换失效**:多次切换后语言无法准确变更。
* **反馈缺失**:在下拉框选择语言后,界面没有即时变化,必须保存后才能看到效果。
* **逻辑冗余**:代码中对翻译键(Translation Key)的预处理逻辑不当,导致原始键值在翻译后被覆盖丢失。

## 2. 优化方案与修复内容

### 2.1 模板化翻译 (HTML Pipe)
* **修改前**:在 TypeScript 代码中遍历语言列表,手动调用翻译服务替换文本。这种方式会导致原始的翻译键(如 `dlg.app-language-zh-cn`)被替换为静态字符串(如 `中文`),使得后续无法再次响应语言变更。
* **优化后**:移除了代码中的预处理逻辑,改在 HTML 模板中直接使用 `{{ language.text | translate }}`。
* **收益**:系统始终保留原始翻译键,确保无论如何频繁切换,UI 都能根据当前的语言包正确显示。

### 2.2 实时预览机制 (Real-time Preview)
* **修复内容**:在 `AppSettingsComponent` 的语言变更回调 `onLanguageChange` 中加入了 `this.translateService.use(language)` 调用。
* **收益**:现在当您在下拉框选择语言时,编辑器界面会**立即同步变换**,用户无需等到点击“确定”按钮即可预览切换效果。

### 2.3 状态回滚逻辑 (Rollback)
* **修复内容**:在“取消”按钮的回调逻辑中增加了语言恢复指令。
* **收益**:如果您在预览了某种语言后决定不保存并点击“取消”,界面语言会自动回滚到修改前的状态,保证了设置操作的严谨性。

### 2.4 核心服务清理
* **优化内容**:修复了 `SettingsService` 中重复注入翻译服务实例的问题,统一使用单例服务管理全局语言状态。

---

## 3. 技术原理简述

FUXA 的多语言系统由两部分组成,本次修复主要针对 **UI 框架语言**:

1. **UI 框架语言 (ngx-translate)**:
* 管理菜单、对话框、提示信息等静态文本。
* 资源文件位于 `client/src/assets/i18n/*.json`。
* 本次优化确保了该系统在 Electron 和 Web 环境下的响应一致性。

2. **工程内容语言 (LanguageService)**:
* 管理用户在工程中自定义的变量名、画面文本(带有 `@` 前缀)。
* 数据存储在 `project.fuxap.db` 数据库中。

---

## 4. 常见问题 (FAQ)

**Q: 为什么切换了语言后,我画面里的按钮文字还是旧的?**
A: 请确认您的画面文本是否使用了“多语言文本”功能(即文本前带有 `@` 符号)。画面内的具体业务内容由工程数据库控制,需要在“文本设置”菜单中单独定义翻译。

**Q: 在 Electron App 模式下,语言设置保存在哪里?**
A: 语言配置保存在每个项目的独立配置文件中,路径为:`[您的项目文件夹]/data/_appdata/mysettings.json` 中的 `language` 字段。

---
*文档更新日期:2026-01-28*
123 changes: 123 additions & 0 deletions SETTINGS_AND_DATABASE_GUIDE_ZH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# FUXA 设置与数据库架构说明文档

本文档详细介绍了 FUXA 项目的配置参数以及数据库表格结构,旨在帮助开发者和运维人员更好地理解和管理系统。

---

## 一、 系统设置 (settings.js)

FUXA 的核心设置文件位置取决于运行模式:
* **Standalone (独立运行)**: 通常位于 `server/_appdata/settings.js`。
* **Electron App**: 每个项目有独立的设置,位于 `[项目目录]/data/_appdata/settings.js`。

### 1. 基础配置
* **`uiPort`**: Web 服务器监听的 TCP 端口,默认为 `1881`。
* **`language`**: 编辑器的默认语言,默认为 `en` (英语)。
* **`logDir`**: 日志文件的存储目录,默认为 `_logs`。
* **`logApiLevel`**: HTTP 请求日志的详细程度。可选值:`dev`, `combined`, `common`, `short`, `tiny`, `none`。
* **`dbDir`**: 运行时数据库(如 DAQ 数据)的存储目录,默认为 `_db`。

### 2. 数据采集与分发
* **`daqEnabled`**: 是否启用数据采集 (DAQ),默认为 `true`。
* **`daqTokenizer`**: DAQ 数据库文件的切换周期(单位:小时),默认为 `24`。设为 `0` 则不切换,仅使用一个文件。
* **`broadcastAll`**:
* `false` (默认): 仅将当前视图绑定的变量值推送到前端。
* `true`: 将所有配置的变量值推送到前端。

### 3. 网络与安全
* **`uiHost`**: 绑定的主机地址。默认为 `0.0.0.0`(接受所有接口连接)。
* **`allowedOrigins`**: 允许跨域请求 (CORS) 的源列表。
* **`secureEnabled`**: 是否启用安全认证(登录、权限控制)。
* **`secretCode`**: 用于 JWT 令牌加密的密钥。
* **`tokenExpiresIn`**: 令牌过期时间,例如 `'1h'` (1小时), `'1d'` (1天)。

### 4. 其他功能
* **`swaggerEnabled`**: 是否启用 Swagger API 文档(开发调试用)。
* **`nodeRedEnabled`**: 是否启用集成 Node-RED 流程引擎。
* **`webcamSnapShotsDir`**: 摄像头抓拍图片的存储目录。

---

## 二、 Electron 桌面应用运行模式说明

当使用 FUXA 作为 Electron 桌面应用运行时,文件存储结构与 Standalone 模式有所不同,具有“多项目管理”的特性。

### 1. 软件全局配置 (Config)
存储 Electron 应用本身的配置(如最近打开的项目列表、自启动设置、全屏/右键菜单开关等)。
* **Windows**: `%APPDATA%/fuxa-app/config.json`
* **Linux**: `~/.local/share/fuxa-app/config.json`
* **macOS**: `~/Library/Application Support/fuxa-app/config.json`

### 2. 项目数据结构
在 Electron 模式下,您可以创建多个项目文件夹,每个项目的结构如下:
* **项目根目录**: 您在创建项目时选择的文件夹。
* **`data/`**: 项目的核心数据根目录。
* **`_appdata/`**: 存储配置和数据库。
* `settings.js`: 该项目的独立系统设置。
* `project.fuxap.db`: 该项目的工程配置文件(视图、设备等)。
* `users.fuxap.db`: 该项目的用户信息数据库。
* **`_db/`**: 历史数据目录。
* `daq-data_*.db`: 历史趋势数据。
* **`_logs/`**: 该项目的运行日志。

---

## 三、 数据库架构 (project.fuxap.db)

FUXA 使用 SQLite 数据库存储工程配置信息。文件位置:
* **Standalone**: `server/_appdata/project.fuxap.db`
* **Electron**: `[项目目录]/data/_appdata/project.fuxap.db`

### 1. `general` (通用配置表)
存储工程的全局属性。
* **`name`**: 配置项名称 (如 `version`, `layout`, `charts`, `languages`)。
* **`value`**: 配置内容的 JSON 字符串。

### 2. `views` (视图表)
存储 HMI 编辑器中创建的所有画面(视图)。
* **`name`**: 视图 ID (`v_` 开头)。
* **`value`**: 包含 SVG 内容、控件配置、动画等的 JSON 字符串。

### 3. `devices` (设备表)
存储通信驱动配置(如 PLC, Modbus, MQTT, OPC UA 等)。
* **`name`**: 设备 ID 或 `'server'` (服务端虚拟设备)。
* **`value`**: 包含设备连接参数和变量 (Tags) 定义的 JSON 字符串。

### 4. `texts` (多语言文本表)
存储工程中自定义的多语言文本翻译。
* **`name`**: 文本标识名。
* **`value`**: 包含各语种对应值的 JSON 字符串。

### 5. `alarms` (报警配置表)
存储报警条件的定义。
* **`name`**: 报警名称。
* **`value`**: 包含报警触发条件、严重程度、动作等的 JSON 字符串。

### 6. `notifications` (通知表)
存储系统通知(如邮件、短信)的配置。
* **`name`**: 通知 ID。
* **`value`**: 包含接收者、触发模式等的 JSON 字符串。

### 7. `scripts` (脚本表)
存储工程中编写的 JavaScript 脚本。
* **`name`**: 脚本 ID。
* **`value`**: 脚本代码及执行参数。

### 8. `reports` (报表表)
存储自动生成报表的模板和计划。
* **`name`**: 报表 ID。
* **`value`**: 报表定义 JSON。

### 9. `locations` (位置/地图表)
存储地理信息或位置标记相关的配置。
* **`name`**: 位置 ID。
* **`value`**: 经纬度及关联数据。

---

## 四、 其他运行时数据库

* **`users.fuxap.db`**: 存储用户信息、角色及权限。
* **`alarms.fuxap.db`**: 存储报警的历史记录。
* **`daq-data_*.db`**: 在 `_db` 目录下,存储历史趋势数据(变量值随时间的变化)。
* **`currentTagReadings.db`**: 存储变量的当前最新实时值,用于系统崩溃后恢复状态。
23 changes: 19 additions & 4 deletions app/electron/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,14 +333,29 @@ function getHtmlPath(filename) {
}

function getServerPath() {
// In packaged app, server is at same level as main.js due to files: ["server/**/*"]
const fs = require('fs');
const prodPath = require('path').join(__dirname, 'server/main.js');
const path = require('path');

// 1. Packaged app: server is in the resources folder (extraResources)
// __dirname is usually 'resources/app' or 'resources/app.asar'
const prodPath = path.join(__dirname, '../server/main.js');
if (fs.existsSync(prodPath)) {
return prodPath;
}
// Fall back to development path (app/electron is at app/electron, server is at project root)
return require('path').join(__dirname, '../../server/main.js');

// 2. Local development: app/electron/main.js, server is at project root
const devPath = path.join(__dirname, '../../server/main.js');
if (fs.existsSync(devPath)) {
return devPath;
}

// 3. Fallback for potential files mapping in package.json
const localPath = path.join(__dirname, 'server/main.js');
if (fs.existsSync(localPath)) {
return localPath;
}

return null;
}

// Create main window
Expand Down
37 changes: 30 additions & 7 deletions app/electron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,38 @@
"build": {
"appId": "com.frangoteam.fuxa",
"productName": "FUXA",
"asar": false,
"asar": true,
"asarUnpack": [
"**/node_modules/koffi/**/*",
"**/node_modules/node-epics-ca/**/*"
],
"files": [
"main.js",
"*.html",
"server/**/*",
"client/dist/**/*",
"icons/**/*"
],
"extraResources": [
{
"from": "server/node_modules",
"to": "app/server/node_modules",
"from": "../../server",
"to": "server",
"filter": [
"**/*",
"!node_modules",
"!_logs",
"!_db"
]
},
{
"from": "../../server/node_modules",
"to": "server/node_modules",
"filter": [
"!**/.bin/**"
"!**/.bin",
"**/*"
]
},
{
"from": "../../client/dist",
"to": "client/dist"
}
],
"directories": {
Expand All @@ -45,11 +62,17 @@
"target": [
{
"target": "nsis",
"arch": ["x64"]
"arch": [
"x64"
]
}
],
"icon": "icons/fuxa-logo.ico"
},
"nsis": {
"oneClick": true,
"allowToChangeInstallationDirectory": false
},
"linux": {
"target": [
{
Expand Down
2 changes: 1 addition & 1 deletion client/src/app/_helpers/auth-interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class AuthInterceptor implements HttpInterceptor {

if (user) {
let locuser = {user: user.username, groups: user.groups};
req = req.clone({ headers: req.headers.set(USER_HEADER_KEY, JSON.stringify(locuser)) });
req = req.clone({ headers: req.headers.set(USER_HEADER_KEY, encodeURIComponent(JSON.stringify(locuser))) });
}
req = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, token) });
}
Expand Down
19 changes: 17 additions & 2 deletions client/src/app/_models/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class Device {
id: 'Device id, GUID',
name: 'Device name',
enabled: 'Enabled',
type: 'Device Type: FuxaServer | SiemensS7 | OPCUA | BACnet | ModbusRTU | ModbusTCP | WebAPI | MQTTclient | internal | EthernetIP | ADSclient | Gpio | WebCam | MELSEC | REDIS',
type: 'Device Type: FuxaServer | SiemensS7 | OPCUA | BACnet | ModbusRTU | ModbusTCP | WebAPI | MQTTclient | internal | EthernetIP | ADSclient | Gpio | WebCam | MELSEC | REDIS | EPICS',
polling: 'Polling interval in millisec., check changed value after ask value, by OPCUA there is a monitor',
property: 'Connection property depending of type',
tags: 'Tags list of Tag',
Expand Down Expand Up @@ -103,6 +103,10 @@ export class Tag {
*/
direction?: string;
edge?: string;
/**
* Optional EPICS monitor flag, if true use real-time monitor mode
*/
monitor?: boolean;


constructor(_id: string) {
Expand All @@ -127,6 +131,7 @@ export class Tag {
format: 'Number of digits to appear after the decimal point',
direction: 'A string specifying whether the GPIO should be configured as an input or output. The valid values are: \'in\', \'out\', \'high\', and \'low\'. If \'out\' is specified the GPIO will be configured as an output and the value of the GPIO will be set to 0. \'high\' and \'low\' are variants of \'out\' that configure the GPIO as an output with an initial level of 1 or 0 respectively.',
edge: 'An optional string specifying the interrupt generating edge or edges for an input GPIO. The valid values are: \'none\', \'rising\', \'falling\' or \'both\'. The default value is \'none\' indicating that the GPIO will not generate interrupts. Whether or not interrupts are supported by an input GPIO is GPIO specific. If interrupts are not supported by a GPIO the edge argument should not be specified. The edge argument is ignored for output GPIOs.',
monitor: 'A boolean flag for EPICS tags indicating whether to use real-time monitor mode. If true, the EPICS Channel Access client will subscribe to PV changes and receive updates in real-time. If false or undefined, the tag will be polled at regular intervals.',
};
}

Expand Down Expand Up @@ -249,7 +254,8 @@ export enum DeviceType {
GPIO = 'GPIO',
WebCam = 'WebCam',
MELSEC = 'MELSEC',
REDIS = 'REDIS'
REDIS = 'REDIS',
EPICS = 'EPICS'
// Template: 'template'
}

Expand Down Expand Up @@ -390,6 +396,15 @@ export enum GpioEdgeType {
both = 'both',
}

/**
* EPICS Tag data types
*/
export enum EpicsTagType {
String = 'string',
Number = 'number',
Boolean = 'boolean'
}

export enum MessageSecurityMode {
/** The MessageSecurityMode is invalid */
INVALID,
Expand Down
7 changes: 3 additions & 4 deletions client/src/app/_services/settings.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,15 @@ export class SettingsService {
private editModeLocked = false;

constructor(private http: HttpClient,
private fuxaLanguage: TranslateService,
private translateService: TranslateService,
private toastr: ToastrService) {
}

init() {
// this language will be used as a fallback when a translation isn't found in the current language
this.fuxaLanguage.setDefaultLang('en');
this.translateService.setDefaultLang('en');
// the lang to use, if the lang isn't available, it will use the current loader to get them
this.fuxaLanguage.use('en');
this.translateService.use('en');
// to load saved settings
if (environment.serverEnabled) {
this.http.get<any>(this.endPointConfig + '/api/settings').subscribe(result => {
Expand All @@ -46,7 +45,7 @@ export class SettingsService {
setSettings(settings: AppSettings) {
var dirty = false;
if (settings.language && settings.language !== this.appSettings.language) {
this.fuxaLanguage.use(settings.language);
this.translateService.use(settings.language);
this.appSettings.language = settings.language;
dirty = true;
}
Expand Down
2 changes: 2 additions & 0 deletions client/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ import { ApiKeysListComponent } from './apikeys/api-keys-list/api-keys-list.comp
import { ApiKeyPropertyComponent } from './apikeys/api-key-property/api-key-property.component';
import { TagPropertyEditRedisComponent } from './device/tag-property/tag-property-edit-redis/tag-property-edit-redis.component';
import { TagPropertyRedisScanComponent } from './device/tag-property/tag-property-edit-redis/tag-property-redis-scan/tag-property-redis-scan.component';
import { TagPropertyEditEpicsComponent } from './device/tag-property/tag-property-edit-epics/tag-property-edit-epics.component';

export function createTranslateLoader(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
Expand Down Expand Up @@ -420,6 +421,7 @@ export const myCustomTooltipDefaults: MatTooltipDefaultOptions = {
ApiKeyPropertyComponent,
TagPropertyEditRedisComponent,
TagPropertyRedisScanComponent,
TagPropertyEditEpicsComponent,
],
imports: [
BrowserModule,
Expand Down
Loading