ActDenox 智能脱硝控制系统

目录

  1. 快速开始
  2. Modbus 通讯管理
  3. 内部变量监控
  4. 内部变量完整参考
  5. 字段映射
  6. 数据保存与查询
  7. 历史趋势
  8. PID 控制
  9. 仪表盘
  10. 数据过滤器
  11. 仪表状态补偿
  12. 报警阈值配置
  13. 用户管理与授权
  14. 系统工具与备份
  15. 配置文件与数据文件参考
  16. 常见问题
  17. 开发与打包清单

一、快速开始

1.1 启动与登录

  • 双击运行程序,首页显示系统状态和功能入口
  • 首次运行自动创建三个账号:
账号 密码 角色 权限范围
admin admin123 管理员 全部功能
operator operator123 操作员 日常操作(无用户管理)
viewer viewer123 观察者 仅查看
  • 自动登录:用户管理页面可开启 " 自动登录 “,电脑重启后自动以操作员身份运行,无需人工干预

1.2 基本工作流

  1. Modbus 通讯管理 — 连接设备(主机模式)或对外提供寄存器服务(从机模式),配置解析规则
  2. 字段映射 — 将解析字段映射到数据库列名
  3. 数据保存与查询 — 启动自动保存,查询历史数据,导出 Excel
  4. (可选)PID 控制 — 配置控制回路,自动计算输出值
  5. (可选)报警配置 — 配置看门狗心跳和阈值报警规则
  6. (可选)仪表盘 — 自定义数据看板,拖拽组件

二、Modbus 通讯管理

2.1 主机模式 — 读取设备数据

RTU 连接

  1. 选择模式:主机 (Master)
  2. 选择协议:Modbus RTU (串口)
  3. 填入串口号(如 COM1)、波特率、校验位、停止位
  4. 点击 连接
  5. 读写操作:设置地址、数量、数据类型,点击读取写入

TCP 连接

  1. 选择协议:Modbus TCP (以太网)
  2. 填入 IP 地址和端口(默认 502)
  3. 点击 连接

后台轮询

  • 点击 启动后台轮询,程序持续读取并更新内部变量
  • 间隔建议 1000ms
  • 页面切换后继续运行

解析规则表

说明
起始地址 从机模式下可见,主机模式下隐藏
字段名 内部变量名,需一字不差地匹配
类型 uint16 / int16 / float32 / float64
字节序 BE(大端,标准)/ LE(小端)
字序 ABCD(标准)/ BADC / CDAB / DCBA
备注 自由文本
当前值 实时显示解析结果,绿色=正常,红色=异常
操作 ↑↓ 移动行 / 删除

2.2 从机模式 — 对外提供寄存器服务

程序作为 Modbus 从站,DCS/PLC 可通过标准 Modbus 协议读写本机寄存器。

  1. 选择模式:从机 (Slave)
  2. 配置端口(如 COM2)、波特率、站号
  3. 在解析规则表中填好 " 起始地址 " 列
  4. 点击 启动从机

数据流向

1
2
3
DCS 写寄存器 → 从机接收 → internalVariables['字段名'] = 值
PID/报警内部计算 → internalVariables['输出变量'] = 值
DCS 读寄存器 ← 从机响应 ← internalVariables['输出变量']
  • DCS 写入的 CEMS 数据:在解析规则表中有地址的字段自动接收
  • PID 输出/报警状态:在解析规则表中有地址的字段自动暴露给 DCS

2.3 通讯诊断

指标 说明
总请求数 发送/接收的总帧数
成功/失败 主机读写成功/失败次数
成功率 成功次数 / 总次数
超时次数 操作超时计数
连接断开 串口异常断开次数
CRC/校验失败 帧校验错误次数
异常响应 Modbus 异常码响应次数

三、内部变量监控

3.1 变量分类与筛选

页面顶部筛选栏:全部 | Modbus | PID | 手动 | 计算 | 系统

  • Modbus:解析规则表中的字段,数据来自 Modbus 读取或从机接收
  • PID:PID 控制回路产生的变量(P/I/D 分量、输出值、健康指标)
  • 手动:用户手动添加的变量
  • 计算:用户自定义表达式计算出的变量
  • 系统:程序运行状态指标(_sys_ 前缀)

3.2 手动添加变量

填写变量名和初始值 → 点击添加变量。变量值可点击直接编辑。

手动创建的变量标记为可写变量,可在仪表盘 " 可编辑数值 " 组件中选择,点击直接修改值。

3.3 计算变量

输入表达式,引用其他变量名进行数学计算。示例:

  • 入口NOX * 0.8 + 出口NOX * 0.2(加权平均)
  • (PID-1_output + 50) / 2(映射到 0-100 范围)
  • 出口NOX / 入口NOX * 100(转化率百分比)

支持运算符:+ - * / ( )。变量不存在时静默跳过不报错。

3.4 搜索

筛选栏右侧搜索框,输入关键字即时过滤。


四、内部变量完整参考

4.1 系统运行状态(_sys_ 前缀,只读)

变量名 类型 单位 说明
_sys_uptime 数值 程序已运行时间
_sys_memory_rss_mb 数值 MB 进程物理内存占用
_sys_memory_heap_mb 数值 MB JS 堆内存占用
_sys_memory_total_mb 数值 MB JS 堆内存总量
_sys_modbus_connected 0/1 主机或从机任一在线即为 1
_sys_modbus_slave_running 0/1 从机是否运行中
_sys_modbus_requests 数值 主机总请求数
_sys_modbus_success 数值 主机成功次数
_sys_modbus_errors 数值 主机失败次数
_sys_modbus_rate 数值 % 主机成功率
_sys_modbus_consecutive_errors 数值 连续错误数(≥3 触发重连)
_sys_modbus_healthy 0/1 通讯健康状态
_sys_modbus_refresh_failures 数值 后台轮询连续失败次数
_sys_background_active 0/1 后台轮询是否运行中
_sys_pid_total 数值 PID 实例总数
_sys_pid_active 数值 活跃 PID 实例数
_sys_alarm_enabled 0/1 报警引擎是否启用
_sys_alarm_active_rules 数值 当前触发的报警规则数
_sys_auto_save_active 0/1 自动保存是否运行中
_sys_db_datastore_mb 数值 MB 主数据库文件大小
_sys_db_write_failures 数值 连续数据库写入失败次数
_sys_db_write_healthy 0/1 数据库写入健康(0=不健康)
_sys_db_retention_days 数值 数据保留天数
_sys_db_last_cleanup_count 数值 上次清理删除的数据行数
_sys_variable_count 数值 内部变量总数(不含系统变量)
_sys_variable_limit_reached 0/1 变量数是否达上限(500)
_sys_compensation_active_count 数值 当前活跃的补偿规则数

4.2 PID 控制变量(以 PID-1 为例,多实例替换名称)

变量名 类型 方向 说明
PID-1_P 数值 输出 比例项实时值
PID-1_I 数值 输出 积分项实时值
PID-1_D 数值 输出 微分项实时值
PID-1_output 数值 输出 PID 修正量输出值
PID-1_abs_error 数值 输出 当前绝对误差 |SP - PV|
PID-1_divergence_seconds 数值 输出 偏离超阈值已持续秒数(0=正常)
PID-1_saturation 0/1/2 输出 输出饱和:0=正常, 1=上限饱和, 2=下限饱和
PID-1_reset_trigger 0/1 输入 DCS 写 1 → PID 自动复位(清零积分 + 历史),复位后自动归 0
PID-1_integralFreeze 0/1 输入 写 1 冻结积分累积,写 0 恢复
PID-1_integralFreeze(补偿) 0/1 输出 补偿引擎自动控制的积分冻结标志
<输出目标> 数值 输出 PID 最终输出值(基准 + PID 修正),等于配置的 outputTarget
<输出目标>_SP 数值 输出 当前设定值

4.3 报警变量

变量名 类型 说明
_alarm_heartbeat 数值 看门狗心跳计数(0-65535 循环递增)
<规则名>_alarm 0/1 该报警规则当前是否触发(1=报警中, 0=正常)
_alarm_active_<规则名> 字符串 报警级别(warning/alarm/critical),内部使用

4.4 补偿变量

变量名 说明
<字段名>(补偿) 补偿引擎处理后的值。原始值保留在 <字段名> 不变
<字段名>(过滤) 数据过滤器处理后的值
<规则名>_compensating 该补偿规则是否正在执行中(1=补偿中, 0=正常)

五、字段映射

  • 将内部变量名(sourceName)映射到数据库列名(dbField)
  • 保存数据时,系统从内部变量读取值,写入数据库对应列
  • 点击 从规则和 PID 加载 自动填充所有已知字段
  • 可手动添加行
  • 数据库列管理:查看所有列、删除不需要的列

六、数据保存与查询

6.1 自动保存

  • 设置保存间隔(秒),点击 启动自动保存
  • 程序定时将勾选的字段值写入 SQLite 数据库
  • 数据文件:data/datastore.sqlite

6.2 保存变量选择

  • 点击 刷新数据库字段列表 加载当前映射
  • 勾选需要保存的字段,取消勾选的字段不会写入数据库
  • 全选 / 取消全选 快捷操作

6.3 数据查询

  • 聚合模式:原始数据 / 分钟均值 / 小时均值 / 天均值
  • 时间范围:预设快捷按钮(最近 1 小时/1 天/7 天/30 天)或手动输入起止时间
  • 显示条数:仅影响表格展示行数
  • 查询 按钮执行查询
  • 导出 Excel:导出选中时间范围内全部数据(不受显示条数限制),上限 50000 条

6.4 数据保留策略

  • 保留天数默认 180 天(6 个月),可修改
  • 每天凌晨 3:00 自动清理超过保留期的旧数据
  • 页面显示上次清理时间和删除行数

七、历史趋势

  • 选择聚合模式和时间范围 → 点击 查询
  • 字段选择面板:勾选要显示的字段(只有已选保存的字段才有历史数据)
  • Y 轴分配:1-3 个独立轴,支持手动设量程
  • 底部滑块缩放时间范围
  • 鼠标悬停查看具体数值
  • 图例点击切换曲线显隐

八、PID 控制

8.1 参数说明

参数 说明 建议
控制目标 (PV) 要控制的内部变量名 通常是 Modbus 解析的 CEMS 字段
设定值 (SP) 期望达到的目标值 如排放限值 50 mg/m³
Kp 比例系数:误差越大输出越大 先调这个
Ki 积分系数:消除稳态误差 太大易振荡,先设小值
Kd 微分系数:抑制超调 对噪声敏感,建议先设 0
修正下限/上限 PID 修正量的限幅 防止过度调节
基准频率 PID 修正前的基准输出值 喷氨泵正常工作频率
输出下限/上限 最终输出值的安全范围 保护执行机构
采样间隔 每次 PID 计算的间隔 建议 500-2000ms
偏差带 (±) 图表上 SP ± X 画两条虚线 0=不显示,视觉辅助
偏离阈值 (%) SP × X% 超过后开始计时偏离 默认 10%,决定 " 偏差正常 " 判断
PV 滤波 (α) 低通滤波系数,0=强滤波, 1=无滤波 测量噪声大时设为 0.3-0.5
死区 |error| < 此值时不做调节 防止频繁微小调整
输出变化率限制 每秒最大变化量,0=不限 防止执行机构剧烈动作
输出寄存器地址 PID 计算结果写入的 Modbus 寄存器 主机模式下才生效
字节序/字序 输出寄存器的字节/字排列 float32 时需与设备一致

8.2 健康指示灯

PID 运行控制区显示两个实时状态:

指示灯 数据源 绿色 (正常) 红色/橙色 (异常)
输出饱和 PID-1_saturation 输出正常 上限饱和(红)/ 下限饱和(橙)
偏差状态 PID-1_divergence_seconds 偏差正常 持续偏离 + 时长

8.3 远程复位

DCS 通过 Modbus 从机写入 PID-1_reset_trigger = 1 → PID 自动清零(积分归零、历史清空)→ 自动写回 0。只响应 0→1 上升沿,DCS 持续保持 1 不会反复触发。


九、仪表盘

9.1 组件类型

组件 说明 数据源
数值卡片 显示一个或多个变量值 实时 / 聚合(分钟/小时/天均值)
可编辑数值 多变量显示,点击直接修改值 仅可写变量(手动变量/PID 可写参数)
状态指示灯 根据阈值显示正常/报警状态 实时值 + 自定义条件
折线图/柱状图 历史趋势曲线/柱状图 数据库聚合查询
环形仪表盘 单值仪表盘显示 实时 / 聚合
进度条 百分比进度条 实时值 + 独立量程
图片 显示网络图片 URL

9.2 操作

  • 添加组件:点击工具栏「+ 添加组件」
  • 编辑:卡片右上角 ✎ 按钮
  • 删除:卡片右上角 × 按钮
  • 尺寸切换:点击尺寸标签(小/中/大/特大),四档循环
  • 拖拽排序:拖拽卡片到目标位置交换
  • 保存/加载布局:工具栏按钮,布局存入备份

9.3 聚合模式

数值卡片和环形仪表盘支持查询数据库聚合数据(分钟均值/小时均值/天均值),显示最近一个时段的值。图表支持多时段折线。

9.4 可编辑数值(多数据源 + 显示名称)

可编辑数值组件允许添加多个可写变量,点击数值直接修改,常用于 DCS 目标值下发等场景。

数据源过滤:添加数据源时,下拉框只显示可写变量

  • 手动创建的内部变量(变量监控页面手动添加)
  • PID 可写参数(PID-1_SPPID-1_reset_triggerPID-1_integralFreeze 等)
  • Modbus 解析字段、计算变量、系统变量等只读变量不会出现在选项中

显示名称:每个数据源行可单独设置显示名称(如将 PID-1_SP 显示为 " 标干 NOX 目标值 “),未设置时显示原始变量名。

修改数值:点击组件中的数值即可编辑,输入后按回车或点击其他区域保存。修改通过 setInternalVariable 写入内部变量,PID 控制回路会立即响应(如修改了 _SP 设定值)。


十、数据过滤器

按需添加过滤规则,只显示已配置的规则。未配置的字段直接透传。

过滤器类型

类型 参数 说明
范围限制 min, max, 越界动作 钳位到边界或拒绝
变化率限制 maxDelta 相邻值变化不超过设定值
移动平均 windowSize 最近 N 次值的算数平均
尖峰抑制 threshold 偏差超过阈值沿用上次有效值

过滤结果写入伴生变量 字段名(过滤),原始值保留不变。


十一、仪表状态补偿

11.1 补偿规则

每条规则包含:

  • 进入条件(IF):满足时进入补偿状态
  • 退出条件(可选):满足时退出补偿状态(无退出条件则为跟随模式)

11.2 补偿动作

动作 说明 示例
保持 (hold) 冻结为进入补偿前的值 仪表吹扫时保持最后好值
偏移 (offset) 当前值 ± 偏移量 NOX + 10
缩放 (scale) 当前值 × 系数 流量 × 1.2
设值 (set) 设为固定值 设为 0
取变量值 (setVar) 取另一变量的当前值 用出口 NOX 替换入口 NOX

设值支持两种模式:固定数值 / 取变量值(点击输入框旁的 " 取变量 " 切换)。

11.3 条件支持变量比较

进入/退出条件除了比较固定阈值,还可以比较两个变量:

  • IF 出口NOX > 入口NOX — 两个变量实时比较
  • IF _sys_modbus_connected == 0 — 系统状态触发补偿

条件行阈值输入框旁有 " 取变量 " 切换按钮。

11.4 补偿状态变量

每条启用的补偿规则实时产生 <规则名>_compensating(1=补偿中, 0=正常)和 _sys_compensation_active_count(活跃规则总数),可在仪表盘显示。


十二、报警阈值配置

12.1 总开关与看门狗

  • 启用报警:开启整个报警引擎
  • 检查间隔:检查频率(ms),默认 1000
  • 看门狗心跳:定时向指定寄存器写入递增计数。DCS 读到计数不变化 = 程序通讯断开

12.2 报警规则

每个规则配置:

  • 规则名:唯一标识(内部变量 <规则名>_alarm 由此生成)
  • 级别:⚠ 预警 / 🚨 报警 / 🔥 严重
  • 条件组合:多条件 AND(全部满足)/ OR(任一满足)
  • 延时:条件持续满足 N 秒后才触发(0 = 立即)
  • 滞后:报警触发后需值回落阈值 - 死区才解除
  • 通知寄存器:报警时写入报警值,恢复时写入正常值

12.3 DCS 如何获取报警状态

将以下变量加入从机解析规则表(填写地址):

变量名 说明
_alarm_heartbeat 看门狗计数,DCS 读这个检查程序是否存活
<规则名>_alarm 该规则报警状态(1=报警中, 0=正常)

12.4 报警延时进度

延时触发中,状态面板实时显示倒计时(剩余 N 秒),到 0 自动触发报警。


十三、用户管理与授权

13.1 用户管理(管理员专属)

  • 添加/删除用户,修改密码
  • 三种角色:管理员(全部权限)/ 操作员(日常操作)/ 观察者(只读)

13.2 权限矩阵

为每个角色勾选可访问的页面。管理员始终拥有全部权限。修改后立即生效。

13.3 自动登录(无人值守)

勾选 " 启用自动登录 " 后,电脑重启/程序自启时自动以操作员身份运行,无需人工登录。确保 PID/报警在无人时持续工作。

13.4 会话超时与降级

会话超时时间可设置(默认 30 分钟)。无操作超时后不退出,而是自动降级为观察者角色。管理员操作后超时不会被完全锁死,程序仍在运行。

13.5 切换账号

首页顶栏和用户管理页面均有「切换账号」按钮,无需先退出即可换账号。


十四、系统工具与备份

14.1 配置备份与恢复

  • 导出配置备份:将所有配置文件打包为一个 JSON 文件
  • 导入配置备份:从备份文件恢复。支持选择性恢复(勾选要恢复的项)
  • 备份文件列表自动扫描 data/ 目录,新增配置自动纳入

14.2 数据备份

每天凌晨 3:00 自动将 data/ 下所有文件备份到 data/backups/YYYY-MM-DD/,保留 30 天。

14.3 数据清理

每天凌晨 3:00 自动删除超过保留天数(默认 180 天)的历史数据。在数据保存页面可调整保留天数。


十五、配置文件与数据文件参考

15.1 文件清单

所有配置和数据存储在 data/ 目录下:

文件 内容 格式
modbus.json Modbus 连接参数 JSON
autosave.json 自动保存配置 JSON
pid.json PID 实例配置 JSON
alarm.json 报警规则 + 看门狗 JSON
save-filter.json 保存变量选择 JSON
manual-variables.json 手动添加的变量 JSON
computed-variables.json 计算变量表达式 JSON
data-filter.json 数据过滤规则 JSON
instrument-compensation.json 补偿规则 JSON
dashboard-layout.json 仪表盘布局 JSON
mapping.json 字段映射表 JSON
pages.json 页面权限配置 JSON
modbus-slave-config.json 从机串口参数 JSON
modbus-parse-rules.json 解析规则(根目录) JSON
datastore.sqlite CEMS 历史数据 SQLite
auth.db 用户/权限/授权/自登录 SQLite
alarm_history.sqlite 报警历史记录 SQLite
audit_log.sqlite 操作审计日志 SQLite
error-YYYY-MM-DD.log 错误日志(按天滚动) 文本

15.2 更新程序是否丢失数据

不会。安装新版本时,安装目录下的程序文件会更新,但 data/ 目录不会被覆盖。所有配置、数据库、布局全部保留。

15.3 查看 SQLite 数据库

推荐使用 DB Browser for SQLite(免费开源),直接拖 .sqlite 文件打开即可浏览数据。


十六、常见问题

Q: 连接设备后读取失败? 检查端口、波特率、校验位、停止位是否与设备匹配。查看报文区域的 hex 帧。

Q: 从机启动没反应? 确保串口未被其他程序占用。COM 口必须存在且配置正确。

Q: 从机 Modbus Poll 超时? 检查 Poll 的站号是否与从机 " 本机站号 " 一致。检查 COM 口和波特率。

Q: 数据查询没有结果? 确认自动保存已启动、保存变量已勾选、字段映射已配置。注意:只有勾选了 " 保存变量选择 " 的字段才有历史数据。

Q: PID 不工作? 确认控制目标字段在内部变量中有值(需要 Modbus 读取或从机接收获取 PV)。

Q: PID 页面显示 " 仅更新内部变量 “? 这表示 Modbus 主机未连接。如果只用从机模式,这是正常的——PID 计算结果通过从机暴露给 DCS 读取,不需要主机写寄存器。

Q: 报警不触发? 确认报警总开关已启用、规则已添加并勾选启用、监控字段在内部变量中已有值。

Q: 变量页面看不到某个字段? 使用搜索框或分类筛选。系统变量以 _sys_ 开头,PID 变量以实例名开头。

Q: 补偿规则中 " 取变量 " 怎么用? 条件行阈值输入框旁点 " 取变量 “,切换为变量选择。可以实现 " 出口 NOX > 入口 NOX” 这样的动态比较。


十七、开发与打包清单

17.1 技术栈

技术
桌面框架 Electron
前端 HTML/CSS/JS + ECharts
后端 Node.js (CommonJS)
数据库 SQLite (sqlite3)
Modbus modbus-serial
Excel xlsx (SheetJS)

17.2 项目结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Electron_Test/
├── main.js              # 主进程入口(窗口、IPC、核心逻辑)
├── preload.js           # 预加载(安全桥梁)
├── configManager.js     # 统一配置管理器
├── variableRegistry.js  # 变量元数据注册表
├── modbusManage.js      # Modbus 主机(读写/连接)
├── modbusSlave.js       # Modbus 从机
├── pidController.js     # PID 控制器类
├── dataStore.js         # SQLite 数据存储
├── alarmManager.js      # 报警引擎
├── authManager.js       # 用户管理/授权
├── auditLogger.js       # 操作审计日志
├── generateLicense.js   # 离线授权码生成工具
├── build-obfuscate.js   # 构建混淆脚本
├── ipc/
   ├── modbusIpc.js     # Modbus IPC 注册
   ├── pidIpc.js        # PID IPC
   ├── alarmIpc.js      # 报警 IPC
   ├── authIpc.js       # 授权 IPC
   ├── dataIpc.js       # 数据 IPC
   ├── variableIpc.js   # 变量 IPC
   └── systemIpc.js     # 系统 IPC
├── pages/
   ├── home.html        # 首页(功能入口)
   ├── dashboard.html   # 仪表盘
   ├── modbus.html      # Modbus 通讯
   ├── variables.html   # 内部变量
   ├── pid.html         # PID 控制
   ├── data.html        # 数据查询
   ├── trend.html       # 历史趋势
   ├── filter.html      # 数据过滤
   ├── compensation.html # 仪表补偿
   ├── alarm.html       # 报警配置
   ├── auth.html        # 用户管理
   ├── mapping.html     # 字段映射
   ├── index.css        # 基础样式
   ├── theme-light.css  # 主题
   ├── layout.js        # 统一导航栏
   ├── authGuard.js     # 权限守卫
   ├── dashboardWidgets.js # 仪表盘组件注册
   └── *.js             # 各页面逻辑
├── package.json         # 项目配置
├── nodemon.json         # 热重载配置
├── modbus-parse-rules.json # 默认解析规则
└── icon.png             # 应用图标

17.3 打包命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 开发模式
npm start        # nodemon 热重载启动

# 生产构建(含代码混淆)
npm run build    # 混淆 → 打包 → 输出到 dist/

# 仅混淆不打包
npm run obfuscate

# 直接运行(无混淆,无热重载)
npm run prod

17.4 构建产物

执行 npm run build 后在 dist/ 目录生成:

  • ActDenox 智能脱硝控制系统 Setup x.x.x.exe — NSIS 安装包

安装程序后,数据目录位于:

  • %APPDATA%/ActDenox 智能脱硝控制系统/data/

17.5 代码保护

打包前自动使用 javascript-obfuscator 混淆所有核心 JS 文件。

  • 原始源码:开发目录,正常可读
  • 构建产物:dist/ 中安装包内的 JS 文件已混淆为不可读形式
  • 页面文件(HTML/CSS)不混淆
  • 混淆仅影响打包产物,不影响开发源码

17.6 首次安装初始化流程

  1. 安装程序复制文件到目标目录
  2. data/ 目录首次创建,所有 JSON 配置由 config.load() 自动生成默认值
  3. auth.db 首次创建,自动建立 admin / operator / viewer 三个账号
  4. 解析规则文件 modbus-parse-rules.json 包含一条默认示例规则
  5. 程序启动 → 自动登录(如启用)→ 恢复上次的运行状态
Licensed under CC BY-NC-SA 4.0