Compare commits
428 Commits
v1.3.1-bet
...
v1.8.8-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8347afb6d0 | ||
|
|
10d0a9763a | ||
|
|
96691d773a | ||
|
|
910509d764 | ||
|
|
158cc39b80 | ||
|
|
940f71aa35 | ||
|
|
788cef8f86 | ||
|
|
baaef862ab | ||
|
|
96db709ced | ||
|
|
c47fa5433f | ||
|
|
440b724798 | ||
|
|
126d6eb163 | ||
|
|
8ac46faf36 | ||
|
|
f112746e12 | ||
|
|
76f21e925b | ||
|
|
a330249c7e | ||
|
|
bbc64c8dc2 | ||
|
|
bb062cf397 | ||
|
|
53bb36ce8d | ||
|
|
8237113577 | ||
|
|
02906ae707 | ||
|
|
6d0e777e68 | ||
|
|
36468c4043 | ||
|
|
1cf7375311 | ||
|
|
ca55948286 | ||
|
|
195f59178f | ||
|
|
e416faf313 | ||
|
|
20bff48cfd | ||
|
|
1a9892b291 | ||
|
|
827953c0a1 | ||
|
|
56e69e3a29 | ||
|
|
1984b63837 | ||
|
|
36b12d5dea | ||
|
|
3e83c8e2c6 | ||
|
|
1fa8c03dff | ||
|
|
d6de9c266b | ||
|
|
2142c1b4f7 | ||
|
|
cafba6c67a | ||
|
|
6eccebbe39 | ||
|
|
5e9f03c900 | ||
|
|
7374bb02d1 | ||
|
|
d19794ae4f | ||
|
|
108cf75cad | ||
|
|
34cbb6be39 | ||
|
|
907f27baf8 | ||
|
|
4710fa7cee | ||
|
|
bfa1f4d0c6 | ||
|
|
737e0d72b0 | ||
|
|
6edeb3e3b8 | ||
|
|
fb00e98a5b | ||
|
|
4f952eff83 | ||
|
|
1eede97c08 | ||
|
|
95419562ee | ||
|
|
7b85d947c4 | ||
|
|
08e670091a | ||
|
|
1348b632a8 | ||
|
|
8cd5162362 | ||
|
|
1c410a89d8 | ||
|
|
6a931fced4 | ||
|
|
622e2eeb90 | ||
|
|
42cd9fff0c | ||
|
|
2a85a438ba | ||
|
|
338b3b1615 | ||
|
|
363f164f47 | ||
|
|
739f04ece9 | ||
|
|
c6ade68ce2 | ||
|
|
7d60e259e2 | ||
|
|
cd71c6a9c1 | ||
|
|
26fe130609 | ||
|
|
95f87b1b05 | ||
|
|
aebdf3e8df | ||
|
|
5d4fc36256 | ||
|
|
b799d785b9 | ||
|
|
d55442fa53 | ||
|
|
c870b749a4 | ||
|
|
4421cbc0c5 | ||
|
|
b6a3305f23 | ||
|
|
5680260136 | ||
|
|
bc66ea6ecb | ||
|
|
1b20218445 | ||
|
|
466e442353 | ||
|
|
9e6751f47f | ||
|
|
f868923905 | ||
|
|
5d3fd6d939 | ||
|
|
37659d687e | ||
|
|
f23b7e2c2f | ||
|
|
55ccdd63d4 | ||
|
|
5c88890ed4 | ||
|
|
fa0185c9a4 | ||
|
|
e1ed9a8e5e | ||
|
|
9f7e3bc282 | ||
|
|
4ec060ba24 | ||
|
|
b1f1753a8d | ||
|
|
94a62f8c31 | ||
|
|
e6f49f256c | ||
|
|
a048b0aa4a | ||
|
|
b6617c140c | ||
|
|
1fdf012f65 | ||
|
|
8fe0321ac0 | ||
|
|
4a2199fa13 | ||
|
|
69680a53a0 | ||
|
|
785dea6545 | ||
|
|
885932afe1 | ||
|
|
a38ccf3dcc | ||
|
|
d7b1f9cc7e | ||
|
|
6e92097544 | ||
|
|
76403b4ddc | ||
|
|
41ad8d8edb | ||
|
|
68754977b0 | ||
|
|
78ad6d5457 | ||
|
|
cb049ebf41 | ||
|
|
f2f5c3201f | ||
|
|
1ba178eae8 | ||
|
|
838f80c30c | ||
|
|
dc0408881e | ||
|
|
1b5e8cb69c | ||
|
|
9e13a3a467 | ||
|
|
32653c3b8a | ||
|
|
16258a2631 | ||
|
|
4c646249ad | ||
|
|
45c8a8d8a9 | ||
|
|
eff3444834 | ||
|
|
25f68f338c | ||
|
|
7ef363a3c2 | ||
|
|
3065e2bece | ||
|
|
1075d66122 | ||
|
|
d477b33774 | ||
|
|
7b374f8618 | ||
|
|
4869c00c0e | ||
|
|
ff4a5a68d9 | ||
|
|
d77a527b15 | ||
|
|
01caca48dc | ||
|
|
43e545b6ae | ||
|
|
8b65956dcc | ||
|
|
7b522ae120 | ||
|
|
43d134d9ad | ||
|
|
24c787f47d | ||
|
|
f95ac8be8c | ||
|
|
41442012c2 | ||
|
|
6f1195dded | ||
|
|
86e8feaabc | ||
|
|
670ca78a5b | ||
|
|
73ceed0f60 | ||
|
|
8412270772 | ||
|
|
686b47898c | ||
|
|
e03eaf2982 | ||
|
|
8b1775b0af | ||
|
|
8bbd04d133 | ||
|
|
26dc6ff6d1 | ||
|
|
24d190921d | ||
|
|
4fec116e93 | ||
|
|
ef6fdaa3ac | ||
|
|
0160d4c53a | ||
|
|
d2a1951b66 | ||
|
|
5d7a0b266a | ||
|
|
ebabd99f2b | ||
|
|
b5e0183e54 | ||
|
|
d450c9ca3e | ||
|
|
894229831d | ||
|
|
bfa24b3665 | ||
|
|
b1446637ad | ||
|
|
02e9c09daf | ||
|
|
21d5cb3858 | ||
|
|
c0a602385d | ||
|
|
4d4dd4ed39 | ||
|
|
3a5d69d96f | ||
|
|
d38a2dea7c | ||
|
|
560d48084a | ||
|
|
625feb3fd1 | ||
|
|
ed4180a072 | ||
|
|
70831b49d2 | ||
|
|
881a120d34 | ||
|
|
8ab1307b2b | ||
|
|
689d474796 | ||
|
|
18bcfb4014 | ||
|
|
645a5f5137 | ||
|
|
1f9743efd0 | ||
|
|
9cef6e6175 | ||
|
|
70b03896d5 | ||
|
|
f9d0ef60b4 | ||
|
|
09ab60f79d | ||
|
|
d21604ea09 | ||
|
|
611b048cb4 | ||
|
|
a2182d9566 | ||
|
|
7a87499aa4 | ||
|
|
b173b72f21 | ||
|
|
29cc107ea5 | ||
|
|
c24766a4db | ||
|
|
29e910ba3c | ||
|
|
1cb240b1b0 | ||
|
|
1525ed9d60 | ||
|
|
b1c2c4d291 | ||
|
|
5585088e3a | ||
|
|
b9073153b3 | ||
|
|
cb554e5d16 | ||
|
|
71d8b5ba0a | ||
|
|
e4f49e9949 | ||
|
|
3afc765f72 | ||
|
|
048714817e | ||
|
|
f267a4e569 | ||
|
|
cfc6479dd5 | ||
|
|
70f24edb53 | ||
|
|
31df4a7f7e | ||
|
|
ca49cf90b4 | ||
|
|
785dbe9533 | ||
|
|
a9d1bc2b50 | ||
|
|
f7766d8cba | ||
|
|
37f8b85924 | ||
|
|
2ed28cf74d | ||
|
|
17698b7fb4 | ||
|
|
386a33ffd8 | ||
|
|
b7d4468cea | ||
|
|
8b0a5d529e | ||
|
|
d18a93f7d2 | ||
|
|
89d1087b1c | ||
|
|
d9e48f5cac | ||
|
|
56377ef595 | ||
|
|
95c300e450 | ||
|
|
2985898b7e | ||
|
|
57d50e6745 | ||
|
|
3dc1a06969 | ||
|
|
fcba935240 | ||
|
|
9b32bb4675 | ||
|
|
f59f484e7b | ||
|
|
0fe9b391d8 | ||
|
|
97f0191875 | ||
|
|
70db8d782c | ||
|
|
2acd166c3e | ||
|
|
c78f7e173a | ||
|
|
99a0b0a55a | ||
|
|
6e51e70b62 | ||
|
|
fd2abad5cf | ||
|
|
b93c6b7c6e | ||
|
|
8b3f558f68 | ||
|
|
8c5289867c | ||
|
|
d179920565 | ||
|
|
504ee0594f | ||
|
|
a92f182156 | ||
|
|
c5d38a5584 | ||
|
|
9bf07e6c3e | ||
|
|
7656936982 | ||
|
|
f477ad9426 | ||
|
|
59950e9d15 | ||
|
|
11d86d92eb | ||
|
|
1275f61113 | ||
|
|
d17e2b8321 | ||
|
|
ce780c3d72 | ||
|
|
76ab4e17bf | ||
|
|
ac1733c46e | ||
|
|
17731de61b | ||
|
|
d85385c1fe | ||
|
|
e0809f52a6 | ||
|
|
b2c55f9d4b | ||
|
|
ba896ea163 | ||
|
|
1a64591256 | ||
|
|
9ce75eb4bd | ||
|
|
451f804f87 | ||
|
|
724d13510d | ||
|
|
3f01ce7ec5 | ||
|
|
df67795239 | ||
|
|
775d4dc95b | ||
|
|
b3c7263bc4 | ||
|
|
9f8732830c | ||
|
|
1c87ef18a1 | ||
|
|
f111b711c5 | ||
|
|
92d1945bea | ||
|
|
4192ea426c | ||
|
|
7bd8f34773 | ||
|
|
bdbb3071b3 | ||
|
|
def02a94d2 | ||
|
|
681bb0963e | ||
|
|
8f7d7c3ead | ||
|
|
c0f45cfb8b | ||
|
|
455653074a | ||
|
|
1202289fab | ||
|
|
acb7b5b4cb | ||
|
|
9bd79dacad | ||
|
|
c544d140bf | ||
|
|
353cb3622b | ||
|
|
b54f15bad6 | ||
|
|
4d9783beca | ||
|
|
5529774b0e | ||
|
|
2a541ef3fe | ||
|
|
e1b4d72ef8 | ||
|
|
6a0992c7a4 | ||
|
|
cff7c724be | ||
|
|
47bda73afe | ||
|
|
8daedc109c | ||
|
|
a164c18a50 | ||
|
|
4d663ada8d | ||
|
|
084a153a79 | ||
|
|
4ed6ec1c63 | ||
|
|
d3c2ba7bee | ||
|
|
55ff9dad4b | ||
|
|
0d6022f5e3 | ||
|
|
8b136d78a8 | ||
|
|
a5214730ef | ||
|
|
9f3ad3caf0 | ||
|
|
9bb2a99be5 | ||
|
|
65ace26258 | ||
|
|
61d975d13f | ||
|
|
1d7d6e9817 | ||
|
|
a2e0999cea | ||
|
|
1edee7f64b | ||
|
|
df61ec2c62 | ||
|
|
bf7a316b04 | ||
|
|
96c4444184 | ||
|
|
59eac62c33 | ||
|
|
ab193db153 | ||
|
|
7d65bba589 | ||
|
|
dcef5a96f0 | ||
|
|
ba0900ac65 | ||
|
|
8f54e95519 | ||
|
|
7b9824b6f9 | ||
|
|
b6835fc7d1 | ||
|
|
bc5a94cd3b | ||
|
|
2a3d868402 | ||
|
|
b998634da1 | ||
|
|
5225380523 | ||
|
|
0e7ea7102c | ||
|
|
8924ba4e99 | ||
|
|
a8071db08e | ||
|
|
fb3c7ded06 | ||
|
|
f6b0a49904 | ||
|
|
bfbbfb164b | ||
|
|
57763702ff | ||
|
|
f469754bb7 | ||
|
|
336662de62 | ||
|
|
9073009eb0 | ||
|
|
ffac6996ed | ||
|
|
da3c92b3bf | ||
|
|
712c7a8f3b | ||
|
|
737afe586d | ||
|
|
d69796b607 | ||
|
|
1cdf66a8c3 | ||
|
|
282c7db3eb | ||
|
|
a02529b9f7 | ||
|
|
af38025f50 | ||
|
|
b28e4d1e95 | ||
|
|
75dfa3ae73 | ||
|
|
4a3455fa60 | ||
|
|
a2fa6570dc | ||
|
|
aef8281ad6 | ||
|
|
d42327a20d | ||
|
|
49db75f34d | ||
|
|
bc87030491 | ||
|
|
9266d7d8e1 | ||
|
|
ef5567ad46 | ||
|
|
302f3d913f | ||
|
|
650b9c1811 | ||
|
|
543ac8c9fe | ||
|
|
7f19f76c0b | ||
|
|
8d76c86309 | ||
|
|
a4be6a6dde | ||
|
|
8b926a0978 | ||
|
|
775ffc8094 | ||
|
|
13a8435f6c | ||
|
|
5d6dda5619 | ||
|
|
f8221ad0f1 | ||
|
|
193aeed6df | ||
|
|
8c87cb3e26 | ||
|
|
1548cd706d | ||
|
|
996272e108 | ||
|
|
ef673bdf1b | ||
|
|
7aa236e987 | ||
|
|
35d857a5f4 | ||
|
|
c2c87ec6c6 | ||
|
|
5ce54d78e1 | ||
|
|
c05a312f6c | ||
|
|
86325d9527 | ||
|
|
5074ca89f0 | ||
|
|
c22b048608 | ||
|
|
7ae3014e06 | ||
|
|
8fa37cc9be | ||
|
|
2c730fbdc2 | ||
|
|
5c21140025 | ||
|
|
7859e5ea4c | ||
|
|
0b5ee8eabc | ||
|
|
1decd88415 | ||
|
|
f1b1f0b75a | ||
|
|
f37f2f009f | ||
|
|
15b071378c | ||
|
|
5ba06dcdaf | ||
|
|
f4e7137eb3 | ||
|
|
cf881548d7 | ||
|
|
0a6fd35f93 | ||
|
|
671f8b55bc | ||
|
|
e16b0d237e | ||
|
|
55dab6d675 | ||
|
|
0f5ae9a756 | ||
|
|
cdff7af431 | ||
|
|
c536b823e7 | ||
|
|
b20d42ff38 | ||
|
|
63147bfab5 | ||
|
|
360609df1f | ||
|
|
9a5fc92c86 | ||
|
|
337fca7e7c | ||
|
|
38fc7189ba | ||
|
|
0b939be412 | ||
|
|
351842c9a1 | ||
|
|
8d248408d4 | ||
|
|
2427561dc5 | ||
|
|
b4932b33b6 | ||
|
|
24a7275543 | ||
|
|
529371a0fd | ||
|
|
69df5edbeb | ||
|
|
153a83e569 | ||
|
|
ef1ec220a4 | ||
|
|
8f2e9fe319 | ||
|
|
86ad2a1069 | ||
|
|
494dc437a5 | ||
|
|
deec1ff1df | ||
|
|
4c35233289 | ||
|
|
de9cb3bd3a | ||
|
|
270726e276 | ||
|
|
dbd74afbe6 | ||
|
|
37a91986e5 | ||
|
|
a892c0cf41 | ||
|
|
b7d90872d5 | ||
|
|
f9aa75a105 | ||
|
|
e313b77abc | ||
|
|
ece7c0fe9a | ||
|
|
4aba79f232 | ||
|
|
e159dbb850 | ||
|
|
513c72ea79 | ||
|
|
2c7b98ba42 | ||
|
|
ff9a18a019 | ||
|
|
81005d3e2c |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ko_fi: mikayla_f
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
labels: "enhancement,feature request"
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
4
.github/workflows/check.yml
vendored
4
.github/workflows/check.yml
vendored
@@ -5,12 +5,10 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- latest
|
||||
- devel
|
||||
jobs:
|
||||
check:
|
||||
@@ -28,4 +26,4 @@ jobs:
|
||||
# --no-max-line-length = Disable warnings for long line lengths
|
||||
# --exclude-files ... = Exclude lockbox library (external) and config files
|
||||
# --globals ... = Override all globals overridden in .vscode/settings.json AND 'os' since CraftOS 'os' differs from Lua's 'os'
|
||||
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http parallel periphemu peripheral read rs settings shell term textutils window
|
||||
args: . --no-max-line-length -i 121 512 542 --exclude-files ./lockbox/* ./*/config.lua --globals os _HOST bit colors fs http keys parallel periphemu peripheral read rs settings shell term textutils window
|
||||
|
||||
79
.github/workflows/manifest.yml
vendored
Normal file
79
.github/workflows/manifest.yml
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
# Deploy installation manifests and shields versions
|
||||
name: Deploy Installation Data
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- devel
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v3.1.3
|
||||
# Generate manifest + shields files for main branch
|
||||
- name: Checkout main
|
||||
id: checkout-main
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'main'
|
||||
clean: false
|
||||
- name: Create outputs folders
|
||||
if: success() || failure()
|
||||
shell: bash
|
||||
run: mkdir deploy; mkdir deploy/manifests; mkdir deploy/manifests/main deploy/manifests/devel
|
||||
- name: Generate manifest and shields for main branch
|
||||
id: manifest-main
|
||||
if: ${{ (success() || failure()) && steps.checkout-main.outcome == 'success' }}
|
||||
run: python imgen.py shields
|
||||
- name: Save main's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-main.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/main
|
||||
# Generate manifest for devel branch
|
||||
- name: Checkout devel
|
||||
id: checkout-devel
|
||||
if: success() || failure()
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: 'devel'
|
||||
clean: false
|
||||
- name: Generate manifest for devel
|
||||
id: manifest-devel
|
||||
if: ${{ (success() || failure()) && steps.checkout-devel.outcome == 'success' }}
|
||||
run: python imgen.py
|
||||
- name: Save devel's manifest
|
||||
if: ${{ (success() || failure()) && steps.manifest-devel.outcome == 'success' }}
|
||||
run: mv install_manifest.json deploy/manifests/devel
|
||||
# All artifacts ready now, upload deploy directory
|
||||
- name: Upload artifacts
|
||||
id: upload-artifacts
|
||||
if: ${{ (success() || failure()) && (steps.manifest-main.outcome == 'success' || steps.manifest-latest.outcome == 'success' || steps.manifest-devel.outcome == 'success') }}
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
# Upload manifest JSON
|
||||
path: 'deploy/'
|
||||
- name: Deploy to GitHub Pages
|
||||
if: ${{ (success() || failure()) && steps.upload-artifacts.outcome == 'success' }}
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
47
.github/workflows/shields.yml
vendored
47
.github/workflows/shields.yml
vendored
@@ -1,47 +0,0 @@
|
||||
# Simple workflow for deploying static content to GitHub Pages
|
||||
name: Deploy Component Versions
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Single deploy job since we're just deploying
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v3.1.3
|
||||
- run: mkdir shields
|
||||
- run: python imgen.py shields
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
# Upload shields JSON
|
||||
path: 'shields/'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
_notes/
|
||||
program.sh
|
||||
/*program.sh
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -5,6 +5,7 @@
|
||||
"colors",
|
||||
"fs",
|
||||
"http",
|
||||
"keys",
|
||||
"parallel",
|
||||
"periphemu",
|
||||
"peripheral",
|
||||
@@ -24,6 +25,7 @@
|
||||
},
|
||||
"Lua.hint.setType": true,
|
||||
"Lua.diagnostics.disable": [
|
||||
"duplicate-set-field"
|
||||
"duplicate-set-field",
|
||||
"inject-field"
|
||||
]
|
||||
}
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright © 2022 - 2023 Mikayla Fischler
|
||||
Copyright © 2022 - 2024 Mikayla Fischler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
47
README.md
47
README.md
@@ -7,29 +7,20 @@ Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fi
|
||||

|
||||

|
||||
|
||||
Mod Requirements:
|
||||
- CC: Tweaked
|
||||
- Mekanism v10.1+
|
||||
### [Join](https://discord.gg/R9NSCkhcwt) the Discord!
|
||||
|
||||
Mod Recommendations:
|
||||
- Advanced Peripherals (adds the capability to detect environmental radiation levels)
|
||||
|
||||
v10.1+ is required due the complete support of CC:Tweaked added in Mekanism v10.1
|
||||
|
||||
There was also an apparent bug with boilers disconnecting and reconnecting when active in my test world on 10.0.24, so it may not even have been an option to fully implement this with support for 10.0.
|
||||

|
||||
|
||||
## Released Component Versions
|
||||
|
||||
### Core
|
||||

|
||||
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
### Utilities
|
||||
|
||||

|
||||
|
||||
### Applications
|
||||
|
||||

|
||||

|
||||
@@ -37,11 +28,23 @@ There was also an apparent bug with boilers disconnecting and reconnecting when
|
||||

|
||||

|
||||
|
||||
## Requirements
|
||||
|
||||
Mod Requirements:
|
||||
- CC: Tweaked
|
||||
- Mekanism v10.1+
|
||||
|
||||
Mod Recommendations:
|
||||
- Advanced Peripherals (adds the capability to detect environmental radiation levels)
|
||||
- Immersive Engineering (provides bundled redstone, though any mod containing bundled redstone will do)
|
||||
|
||||
v10.1+ is required due to the complete support of CC:Tweaked added in Mekanism v10.1
|
||||
|
||||
## Installation
|
||||
|
||||
You can install this on a ComputerCraft computer using either:
|
||||
* `wget https://raw.githubusercontent.com/MikaylaFischler/cc-mek-scada/main/ccmsi.lua`
|
||||
* `pastebin get iUMjgW0C ccmsi.lua`
|
||||
* `pastebin get sqUN6VUb ccmsi.lua`
|
||||
|
||||
## [SCADA](https://en.wikipedia.org/wiki/SCADA)
|
||||
> Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery.
|
||||
@@ -86,14 +89,8 @@ A vaguely-modbus [modbus](https://en.wikipedia.org/wiki/Modbus) communication pr
|
||||
- Input Registers: Multi-Byte Read-Only (analog inputs)
|
||||
- Holding Registers: Multi-Byte Read/Write (analog I/O)
|
||||
|
||||
### Security and Encryption
|
||||
### Security
|
||||
|
||||
TBD, I am planning on AES symmetric encryption for security + HMAC to prevent replay attacks. This will be done utilizing this codebase: https://github.com/somesocks/lua-lockbox.
|
||||
HMAC message authentication is available as a configuration option to prevent replay attacks and generally prevent control or false data reporting within a system's network. This is done utilizing the [lua-lockbox](https://github.com/somesocks/lua-lockbox) project.
|
||||
|
||||
This is somewhat important here as otherwise anyone can just control your setup, which is undeseriable. Unlike normal Minecraft PVP chaos, it would be very difficult to identify who is messing with your system, as with an Ender Modem they can do it from effectively anywhere and the server operators would have to check every computer's filesystem to find suspicious code.
|
||||
|
||||
The other security mitigation for commanding (no effect on monitoring) is to enforce a maximum authorized transmission range, which has been added as a configurable feature.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None yet since the switch to requiring 10.1+!
|
||||
The other, simpler security feature is to enforce a maximum authorized transmission range, which is also a configurable feature on each device.
|
||||
|
||||
11
configure.lua
Normal file
11
configure.lua
Normal file
@@ -0,0 +1,11 @@
|
||||
print("CONFIGURE> SCANNING FOR CONFIGURATOR...")
|
||||
|
||||
if fs.exists("reactor-plc/configure.lua") then require("reactor-plc.configure").configure()
|
||||
elseif fs.exists("rtu/configure.lua") then require("rtu.configure").configure()
|
||||
elseif fs.exists("supervisor/configure.lua") then require("supervisor.configure").configure()
|
||||
elseif fs.exists("coordinator/configure.lua") then require("coordinator.configure").configure()
|
||||
elseif fs.exists("pocket/configure.lua") then require("pocket.configure").configure()
|
||||
else
|
||||
print("CONFIGURE> NO CONFIGURATOR FOUND")
|
||||
print("CONFIGURE> EXIT")
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
local config = {}
|
||||
|
||||
-- port of the SCADA supervisor
|
||||
config.SCADA_SV_PORT = 16100
|
||||
-- port to listen to incoming packets from supervisor
|
||||
config.SCADA_SV_CTL_LISTEN = 16101
|
||||
-- listen port for SCADA coordinator API access
|
||||
config.SCADA_API_LISTEN = 16200
|
||||
-- max trusted modem message distance (0 to disable check)
|
||||
config.TRUSTED_RANGE = 0
|
||||
-- time in seconds (>= 2) before assuming a remote device is no longer active
|
||||
config.SV_TIMEOUT = 5
|
||||
config.API_TIMEOUT = 5
|
||||
|
||||
-- expected number of reactor units, used only to require that number of unit monitors
|
||||
config.NUM_UNITS = 4
|
||||
|
||||
-- alarm sounder volume (0.0 to 3.0, 1.0 being standard max volume, this is the option given to to speaker.play())
|
||||
-- note: alarm sine waves are at half saturation, so that multiple will be required to reach full scale
|
||||
config.SOUNDER_VOLUME = 1.0
|
||||
|
||||
-- true for 24 hour time on main view screen
|
||||
config.TIME_24_HOUR = true
|
||||
|
||||
-- log path
|
||||
config.LOG_PATH = "/log.txt"
|
||||
-- log mode
|
||||
-- 0 = APPEND (adds to existing file on start)
|
||||
-- 1 = NEW (replaces existing file on start)
|
||||
config.LOG_MODE = 0
|
||||
-- true to log verbose debug messages
|
||||
config.LOG_DEBUG = false
|
||||
|
||||
return config
|
||||
1312
coordinator/configure.lua
Normal file
1312
coordinator/configure.lua
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,175 +2,171 @@ local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local util = require("scada-common.util")
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local process = require("coordinator.process")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local dialog = require("coordinator.ui.dialog")
|
||||
|
||||
local print = util.print
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
local DEVICE_TYPE = comms.DEVICE_TYPE
|
||||
local ESTABLISH_ACK = comms.ESTABLISH_ACK
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
local FAC_COMMAND = comms.FAC_COMMAND
|
||||
|
||||
local LINK_TIMEOUT = 60.0
|
||||
|
||||
local coordinator = {}
|
||||
|
||||
-- request the user to select a monitor
|
||||
---@nodiscard
|
||||
---@param names table available monitors
|
||||
---@return boolean|string|nil
|
||||
local function ask_monitor(names)
|
||||
println("available monitors:")
|
||||
for i = 1, #names do
|
||||
print(" " .. names[i])
|
||||
end
|
||||
println("")
|
||||
println("select a monitor or type c to cancel")
|
||||
---@type crd_config
|
||||
local config = {}
|
||||
|
||||
local iface = dialog.ask_options(names, "c")
|
||||
coordinator.config = config
|
||||
|
||||
if iface ~= false and iface ~= nil then
|
||||
util.filter_table(names, function (x) return x ~= iface end)
|
||||
-- load the coordinator configuration<br>
|
||||
-- status of 0 is OK, 1 is bad config, 2 is bad monitor config
|
||||
---@return 0|1|2 status, nil|monitors_struct|string monitors (or error message)
|
||||
function coordinator.load_config()
|
||||
if not settings.load("/coordinator.settings") then return 1 end
|
||||
|
||||
config.UnitCount = settings.get("UnitCount")
|
||||
config.SpeakerVolume = settings.get("SpeakerVolume")
|
||||
config.Time24Hour = settings.get("Time24Hour")
|
||||
|
||||
config.DisableFlowView = settings.get("DisableFlowView")
|
||||
config.MainDisplay = settings.get("MainDisplay")
|
||||
config.FlowDisplay = settings.get("FlowDisplay")
|
||||
config.UnitDisplays = settings.get("UnitDisplays")
|
||||
|
||||
config.SVR_Channel = settings.get("SVR_Channel")
|
||||
config.CRD_Channel = settings.get("CRD_Channel")
|
||||
config.PKT_Channel = settings.get("PKT_Channel")
|
||||
config.SVR_Timeout = settings.get("SVR_Timeout")
|
||||
config.API_Timeout = settings.get("API_Timeout")
|
||||
config.TrustedRange = settings.get("TrustedRange")
|
||||
config.AuthKey = settings.get("AuthKey")
|
||||
|
||||
config.LogMode = settings.get("LogMode")
|
||||
config.LogPath = settings.get("LogPath")
|
||||
config.LogDebug = settings.get("LogDebug")
|
||||
|
||||
local cfv = util.new_validator()
|
||||
|
||||
cfv.assert_type_int(config.UnitCount)
|
||||
cfv.assert_range(config.UnitCount, 1, 4)
|
||||
cfv.assert_type_bool(config.Time24Hour)
|
||||
|
||||
cfv.assert_type_bool(config.DisableFlowView)
|
||||
cfv.assert_type_table(config.UnitDisplays)
|
||||
|
||||
cfv.assert_type_num(config.SpeakerVolume)
|
||||
cfv.assert_range(config.SpeakerVolume, 0, 3)
|
||||
|
||||
cfv.assert_channel(config.SVR_Channel)
|
||||
cfv.assert_channel(config.CRD_Channel)
|
||||
cfv.assert_channel(config.PKT_Channel)
|
||||
|
||||
cfv.assert_type_num(config.SVR_Timeout)
|
||||
cfv.assert_min(config.SVR_Timeout, 2)
|
||||
cfv.assert_type_num(config.API_Timeout)
|
||||
cfv.assert_min(config.API_Timeout, 2)
|
||||
|
||||
cfv.assert_type_num(config.TrustedRange)
|
||||
cfv.assert_min(config.TrustedRange, 0)
|
||||
cfv.assert_type_str(config.AuthKey)
|
||||
|
||||
if type(config.AuthKey) == "string" then
|
||||
local len = string.len(config.AuthKey)
|
||||
cfv.assert_eq(len == 0 or len >= 8, true)
|
||||
end
|
||||
|
||||
return iface
|
||||
end
|
||||
cfv.assert_type_int(config.LogMode)
|
||||
cfv.assert_range(config.LogMode, 0, 1)
|
||||
cfv.assert_type_str(config.LogPath)
|
||||
cfv.assert_type_bool(config.LogDebug)
|
||||
|
||||
-- Monitor Setup
|
||||
|
||||
-- configure monitor layout
|
||||
---@param num_units integer number of units expected
|
||||
---@return boolean success, monitors_struct? monitors
|
||||
function coordinator.configure_monitors(num_units)
|
||||
---@class monitors_struct
|
||||
local monitors = {
|
||||
primary = nil,
|
||||
primary = nil, ---@type table|nil
|
||||
primary_name = "",
|
||||
flow = nil, ---@type table|nil
|
||||
flow_name = "",
|
||||
unit_displays = {},
|
||||
unit_name_map = {}
|
||||
}
|
||||
|
||||
local monitors_avail = ppm.get_monitor_list()
|
||||
local names = {}
|
||||
local available = {}
|
||||
local mon_cfv = util.new_validator()
|
||||
|
||||
-- get all interface names
|
||||
for iface, _ in pairs(monitors_avail) do
|
||||
table.insert(names, iface)
|
||||
table.insert(available, iface)
|
||||
end
|
||||
local names = {}
|
||||
for iface, _ in pairs(ppm.get_monitor_list()) do table.insert(names, iface) end
|
||||
|
||||
-- we need a certain number of monitors (1 per unit + 1 primary display)
|
||||
local num_displays_needed = num_units + 1
|
||||
if #names < num_displays_needed then
|
||||
local message = "not enough monitors connected (need " .. num_displays_needed .. ")"
|
||||
println(message)
|
||||
log.warning(message)
|
||||
return false
|
||||
end
|
||||
local function setup_monitors()
|
||||
mon_cfv.assert_type_str(config.MainDisplay)
|
||||
if not config.DisableFlowView then mon_cfv.assert_type_str(config.FlowDisplay) end
|
||||
mon_cfv.assert_eq(#config.UnitDisplays, config.UnitCount)
|
||||
|
||||
-- attempt to load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.warning("configure_monitors(): failed to load coordinator settings file (may not exist yet)")
|
||||
else
|
||||
local _primary = settings.get("PRIMARY_DISPLAY")
|
||||
local _unitd = settings.get("UNIT_DISPLAYS")
|
||||
if mon_cfv.valid() then
|
||||
local w, h, _
|
||||
|
||||
-- filter out already assigned monitors
|
||||
util.filter_table(available, function (x) return x ~= _primary end)
|
||||
if type(_unitd) == "table" then
|
||||
util.filter_table(available, function (x) return not util.table_contains(_unitd, x) end)
|
||||
end
|
||||
end
|
||||
|
||||
---------------------
|
||||
-- PRIMARY DISPLAY --
|
||||
---------------------
|
||||
|
||||
local iface_primary_display = settings.get("PRIMARY_DISPLAY") ---@type boolean|string|nil
|
||||
|
||||
if not util.table_contains(names, iface_primary_display) then
|
||||
println("primary display is not connected")
|
||||
local response = dialog.ask_y_n("would you like to change it", true)
|
||||
if response == false then return false end
|
||||
iface_primary_display = nil
|
||||
end
|
||||
|
||||
while iface_primary_display == nil and #available > 0 do
|
||||
-- lets get a monitor
|
||||
iface_primary_display = ask_monitor(available)
|
||||
end
|
||||
|
||||
if type(iface_primary_display) ~= "string" then return false end
|
||||
|
||||
settings.set("PRIMARY_DISPLAY", iface_primary_display)
|
||||
util.filter_table(available, function (x) return x ~= iface_primary_display end)
|
||||
|
||||
monitors.primary = ppm.get_periph(iface_primary_display)
|
||||
monitors.primary_name = iface_primary_display
|
||||
|
||||
-------------------
|
||||
-- UNIT DISPLAYS --
|
||||
-------------------
|
||||
|
||||
local unit_displays = settings.get("UNIT_DISPLAYS")
|
||||
|
||||
if unit_displays == nil then
|
||||
unit_displays = {}
|
||||
for i = 1, num_units do
|
||||
local display = nil
|
||||
|
||||
while display == nil and #available > 0 do
|
||||
-- lets get a monitor
|
||||
println("please select monitor for unit #" .. i)
|
||||
display = ask_monitor(available)
|
||||
if not util.table_contains(names, config.MainDisplay) then
|
||||
return 2, "Main monitor is not connected."
|
||||
end
|
||||
|
||||
if display == false then return false end
|
||||
monitors.primary = ppm.get_periph(config.MainDisplay)
|
||||
monitors.primary_name = config.MainDisplay
|
||||
|
||||
unit_displays[i] = display
|
||||
end
|
||||
else
|
||||
-- make sure all displays are connected
|
||||
for i = 1, num_units do
|
||||
local display = unit_displays[i]
|
||||
|
||||
if not util.table_contains(names, display) then
|
||||
println("unit #" .. i .. " display is not connected")
|
||||
local response = dialog.ask_y_n("would you like to change it", true)
|
||||
if response == false then return false end
|
||||
display = nil
|
||||
monitors.primary.setTextScale(0.5)
|
||||
w, _ = ppm.monitor_block_size(monitors.primary.getSize())
|
||||
if w ~= 8 then
|
||||
return 2, util.c("Main monitor width is incorrect (was ", w, ", must be 8).")
|
||||
end
|
||||
|
||||
while display == nil and #available > 0 do
|
||||
-- lets get a monitor
|
||||
display = ask_monitor(available)
|
||||
if not config.DisableFlowView then
|
||||
if not util.table_contains(names, config.FlowDisplay) then
|
||||
return 2, "Flow monitor is not connected."
|
||||
end
|
||||
|
||||
monitors.flow = ppm.get_periph(config.FlowDisplay)
|
||||
monitors.flow_name = config.FlowDisplay
|
||||
|
||||
monitors.flow.setTextScale(0.5)
|
||||
w, _ = ppm.monitor_block_size(monitors.flow.getSize())
|
||||
if w ~= 8 then
|
||||
return 2, util.c("Flow monitor width is incorrect (was ", w, ", must be 8).")
|
||||
end
|
||||
end
|
||||
|
||||
if display == false then return false end
|
||||
for i = 1, config.UnitCount do
|
||||
local display = config.UnitDisplays[i]
|
||||
if type(display) ~= "string" or not util.table_contains(names, display) then
|
||||
return 2, "Unit " .. i .. " monitor is not connected."
|
||||
end
|
||||
|
||||
unit_displays[i] = display
|
||||
end
|
||||
monitors.unit_displays[i] = ppm.get_periph(display)
|
||||
monitors.unit_name_map[i] = display
|
||||
|
||||
monitors.unit_displays[i].setTextScale(0.5)
|
||||
w, h = ppm.monitor_block_size(monitors.unit_displays[i].getSize())
|
||||
if w ~= 4 or h ~= 4 then
|
||||
return 2, util.c("Unit ", i, " monitor size is incorrect (was ", w, " by ", h,", must be 4 by 4).")
|
||||
end
|
||||
end
|
||||
else return 2, "Monitor configuration invalid." end
|
||||
end
|
||||
|
||||
settings.set("UNIT_DISPLAYS", unit_displays)
|
||||
if not settings.save("/coord.settings") then
|
||||
log.warning("configure_monitors(): failed to save coordinator settings file")
|
||||
end
|
||||
if cfv.valid() then
|
||||
local ok, result, message = pcall(setup_monitors)
|
||||
assert(ok, util.c("fatal error while trying to verify monitors: ", result))
|
||||
if result == 2 then return 2, message end
|
||||
else return 1 end
|
||||
|
||||
for i = 1, #unit_displays do
|
||||
monitors.unit_displays[i] = ppm.get_periph(unit_displays[i])
|
||||
monitors.unit_name_map[i] = unit_displays[i]
|
||||
end
|
||||
|
||||
return true, monitors
|
||||
return 0, monitors
|
||||
end
|
||||
|
||||
-- dmesg print wrapper
|
||||
@@ -183,7 +179,8 @@ local function log_dmesg(message, dmesg_tag, working)
|
||||
GRAPHICS = colors.green,
|
||||
SYSTEM = colors.cyan,
|
||||
BOOT = colors.blue,
|
||||
COMMS = colors.purple
|
||||
COMMS = colors.purple,
|
||||
CRYPTO = colors.yellow
|
||||
}
|
||||
|
||||
if working then
|
||||
@@ -197,6 +194,7 @@ function coordinator.log_graphics(message) log_dmesg(message, "GRAPHICS") end
|
||||
function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
|
||||
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
|
||||
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
|
||||
function coordinator.log_crypto(message) log_dmesg(message, "CRYPTO") end
|
||||
|
||||
-- log a message for communications connecting, providing access to progress indication control functions
|
||||
---@nodiscard
|
||||
@@ -212,41 +210,36 @@ end
|
||||
-- coordinator communications
|
||||
---@nodiscard
|
||||
---@param version string coordinator version
|
||||
---@param modem table modem device
|
||||
---@param sv_port integer port of configured supervisor
|
||||
---@param sv_listen integer listening port for supervisor replys
|
||||
---@param api_listen integer listening port for pocket API
|
||||
---@param range integer trusted device connection range
|
||||
---@param nic nic network interface device
|
||||
---@param sv_watchdog watchdog
|
||||
function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range, sv_watchdog)
|
||||
function coordinator.comms(version, nic, sv_watchdog)
|
||||
local self = {
|
||||
sv_linked = false,
|
||||
sv_addr = comms.BROADCAST,
|
||||
sv_seq_num = 0,
|
||||
sv_r_seq_num = nil,
|
||||
sv_config_err = false,
|
||||
connected = false,
|
||||
last_est_ack = ESTABLISH_ACK.ALLOW,
|
||||
last_api_est_acks = {}
|
||||
last_api_est_acks = {},
|
||||
est_start = 0,
|
||||
est_last = 0,
|
||||
est_tick_waiting = nil,
|
||||
est_task_done = nil
|
||||
}
|
||||
|
||||
comms.set_trusted_range(range)
|
||||
comms.set_trusted_range(config.TrustedRange)
|
||||
|
||||
-- configure network channels
|
||||
nic.closeAll()
|
||||
nic.open(config.CRD_Channel)
|
||||
|
||||
-- pass config to apisessions
|
||||
apisessions.init(nic, config)
|
||||
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- configure modem channels
|
||||
local function _conf_channels()
|
||||
modem.closeAll()
|
||||
modem.open(sv_listen)
|
||||
modem.open(api_listen)
|
||||
end
|
||||
|
||||
_conf_channels()
|
||||
|
||||
-- link modem to apisessions
|
||||
apisessions.init(modem)
|
||||
|
||||
-- send a packet to the supervisor
|
||||
---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE
|
||||
---@param msg_type MGMT_TYPE|CRDN_TYPE
|
||||
---@param msg table
|
||||
local function _send_sv(protocol, msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
@@ -261,34 +254,35 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
end
|
||||
|
||||
pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable())
|
||||
s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable())
|
||||
|
||||
modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable())
|
||||
nic.transmit(config.SVR_Channel, config.CRD_Channel, s_pkt)
|
||||
self.sv_seq_num = self.sv_seq_num + 1
|
||||
end
|
||||
|
||||
-- send an API establish request response
|
||||
---@param dest integer
|
||||
---@param msg table
|
||||
local function _send_api_establish_ack(seq_id, dest, msg)
|
||||
---@param packet scada_packet
|
||||
---@param ack ESTABLISH_ACK
|
||||
local function _send_api_establish_ack(packet, ack)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg)
|
||||
s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
m_pkt.make(MGMT_TYPE.ESTABLISH, { ack })
|
||||
s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
modem.transmit(dest, api_listen, s_pkt.raw_sendable())
|
||||
nic.transmit(config.PKT_Channel, config.CRD_Channel, s_pkt)
|
||||
self.last_api_est_acks[packet.src_addr()] = ack
|
||||
end
|
||||
|
||||
-- attempt connection establishment
|
||||
local function _send_establish()
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN })
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRD })
|
||||
end
|
||||
|
||||
-- keep alive ack
|
||||
---@param srv_time integer
|
||||
local function _send_keep_alive_ack(srv_time)
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
@@ -296,98 +290,94 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
---@class coord_comms
|
||||
local public = {}
|
||||
|
||||
-- reconnect a newly connected modem
|
||||
---@param new_modem table
|
||||
function public.reconnect_modem(new_modem)
|
||||
modem = new_modem
|
||||
apisessions.relink_modem(new_modem)
|
||||
_conf_channels()
|
||||
-- try to connect to the supervisor if not already linked
|
||||
---@param abort boolean? true to print out cancel info if not linked (use on program terminate)
|
||||
---@return boolean ok, boolean start_ui
|
||||
function public.try_connect(abort)
|
||||
local ok = true
|
||||
local start_ui = false
|
||||
|
||||
if not self.sv_linked then
|
||||
if self.est_tick_waiting == nil then
|
||||
self.est_start = util.time_s()
|
||||
self.est_last = self.est_start
|
||||
|
||||
self.est_tick_waiting, self.est_task_done =
|
||||
coordinator.log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SVR_Channel)
|
||||
|
||||
_send_establish()
|
||||
else
|
||||
self.est_tick_waiting(math.max(0, LINK_TIMEOUT - (util.time_s() - self.est_start)))
|
||||
end
|
||||
|
||||
if abort or (util.time_s() - self.est_start) >= LINK_TIMEOUT then
|
||||
self.est_task_done(false)
|
||||
|
||||
if abort then
|
||||
coordinator.log_comms("supervisor connection attempt cancelled by user")
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||
elseif not self.sv_linked then
|
||||
if self.last_est_ack == ESTABLISH_ACK.DENY then
|
||||
coordinator.log_comms("supervisor connection attempt denied")
|
||||
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
|
||||
coordinator.log_comms("supervisor connection failed due to collision")
|
||||
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
coordinator.log_comms("supervisor connection failed due to version mismatch")
|
||||
else
|
||||
coordinator.log_comms("supervisor connection failed with no valid response")
|
||||
end
|
||||
end
|
||||
|
||||
ok = false
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
|
||||
ok = false
|
||||
elseif (util.time_s() - self.est_last) > 1.0 then
|
||||
_send_establish()
|
||||
self.est_last = util.time_s()
|
||||
end
|
||||
elseif self.est_tick_waiting ~= nil then
|
||||
self.est_task_done(true)
|
||||
self.est_tick_waiting = nil
|
||||
self.est_task_done = nil
|
||||
start_ui = true
|
||||
end
|
||||
|
||||
return ok, start_ui
|
||||
end
|
||||
|
||||
-- close the connection to the server
|
||||
function public.close()
|
||||
sv_watchdog.cancel()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.sv_linked = false
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- attempt to connect to the subervisor
|
||||
---@nodiscard
|
||||
---@param timeout_s number timeout in seconds
|
||||
---@param tick_dmesg_waiting function callback to tick dmesg waiting
|
||||
---@param task_done function callback to show done on dmesg
|
||||
---@return boolean sv_linked true if connected, false otherwise
|
||||
--- EVENT_CONSUMER: this function consumes events
|
||||
function public.sv_connect(timeout_s, tick_dmesg_waiting, task_done)
|
||||
local clock = util.new_clock(1)
|
||||
local start = util.time_s()
|
||||
local terminated = false
|
||||
|
||||
_send_establish()
|
||||
|
||||
clock.start()
|
||||
|
||||
while (util.time_s() - start) < timeout_s and (not self.sv_linked) and (not self.sv_config_err) do
|
||||
local event, p1, p2, p3, p4, p5 = util.pull_event()
|
||||
|
||||
if event == "timer" and clock.is_clock(p1) then
|
||||
-- timed out attempt, try again
|
||||
tick_dmesg_waiting(math.max(0, timeout_s - (util.time_s() - start)))
|
||||
_send_establish()
|
||||
clock.start()
|
||||
elseif event == "modem_message" then
|
||||
-- handle message
|
||||
local packet = public.parse_packet(p1, p2, p3, p4, p5)
|
||||
if packet ~= nil and packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
public.handle_packet(packet)
|
||||
end
|
||||
elseif event == "terminate" then
|
||||
terminated = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
task_done(self.sv_linked)
|
||||
|
||||
if terminated then
|
||||
coordinator.log_comms("supervisor connection attempt cancelled by user")
|
||||
elseif self.sv_config_err then
|
||||
coordinator.log_comms("supervisor cooling configuration invalid, check supervisor config file")
|
||||
elseif not self.sv_linked then
|
||||
if self.last_est_ack == ESTABLISH_ACK.DENY then
|
||||
coordinator.log_comms("supervisor connection attempt denied")
|
||||
elseif self.last_est_ack == ESTABLISH_ACK.COLLISION then
|
||||
coordinator.log_comms("supervisor connection failed due to collision")
|
||||
elseif self.last_est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
coordinator.log_comms("supervisor connection failed due to version mismatch")
|
||||
else
|
||||
coordinator.log_comms("supervisor connection failed with no valid response")
|
||||
end
|
||||
end
|
||||
|
||||
return self.sv_linked
|
||||
self.sv_r_seq_num = nil
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
|
||||
end
|
||||
|
||||
-- send a facility command
|
||||
---@param cmd FAC_COMMAND command
|
||||
function public.send_fac_command(cmd)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd })
|
||||
---@param option any? optional option options for the optional options (like waste mode)
|
||||
function public.send_fac_command(cmd, option)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, { cmd, option })
|
||||
end
|
||||
|
||||
-- send the auto process control configuration with a start command
|
||||
---@param config coord_auto_config configuration
|
||||
function public.send_auto_start(config)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, {
|
||||
FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
|
||||
---@param auto_cfg coord_auto_config configuration
|
||||
function public.send_auto_start(auto_cfg)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_CMD, {
|
||||
FAC_COMMAND.START, auto_cfg.mode, auto_cfg.burn_target, auto_cfg.charge_target, auto_cfg.gen_target, auto_cfg.limits
|
||||
})
|
||||
end
|
||||
|
||||
-- send a unit command
|
||||
---@param cmd UNIT_COMMAND command
|
||||
---@param unit integer unit ID
|
||||
---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?)
|
||||
---@param option any? optional option options for the optional options (like burn rate)
|
||||
function public.send_unit_command(cmd, unit, option)
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
|
||||
end
|
||||
|
||||
-- parse a packet
|
||||
@@ -396,15 +386,12 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
---@param reply_to integer
|
||||
---@param message any
|
||||
---@param distance integer
|
||||
---@return mgmt_frame|crdn_frame|capi_frame|nil packet
|
||||
---@return mgmt_frame|crdn_frame|nil packet
|
||||
function public.parse_packet(side, sender, reply_to, message, distance)
|
||||
local s_pkt = nic.receive(side, sender, reply_to, message, distance)
|
||||
local pkt = nil
|
||||
local s_pkt = comms.scada_packet()
|
||||
|
||||
-- parse packet as generic SCADA packet
|
||||
s_pkt.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
if s_pkt.is_valid() then
|
||||
if s_pkt then
|
||||
-- get as SCADA management packet
|
||||
if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
local mgmt_pkt = comms.mgmt_packet()
|
||||
@@ -417,12 +404,6 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
if crdn_pkt.decode(s_pkt) then
|
||||
pkt = crdn_pkt.get()
|
||||
end
|
||||
-- get as coordinator API packet
|
||||
elseif s_pkt.protocol() == PROTOCOL.COORD_API then
|
||||
local capi_pkt = comms.capi_packet()
|
||||
if capi_pkt.decode(s_pkt) then
|
||||
pkt = capi_pkt.get()
|
||||
end
|
||||
else
|
||||
log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true)
|
||||
end
|
||||
@@ -432,40 +413,46 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param packet mgmt_frame|crdn_frame|capi_frame|nil
|
||||
---@param packet mgmt_frame|crdn_frame|nil
|
||||
---@return boolean close_ui
|
||||
function public.handle_packet(packet)
|
||||
local was_linked = self.sv_linked
|
||||
|
||||
if packet ~= nil then
|
||||
local l_port = packet.scada_frame.local_port()
|
||||
local r_port = packet.scada_frame.remote_port()
|
||||
local l_chan = packet.scada_frame.local_channel()
|
||||
local r_chan = packet.scada_frame.remote_channel()
|
||||
local src_addr = packet.scada_frame.src_addr()
|
||||
local protocol = packet.scada_frame.protocol()
|
||||
|
||||
if l_port == api_listen then
|
||||
if protocol == PROTOCOL.COORD_API then
|
||||
---@cast packet capi_frame
|
||||
if l_chan ~= config.CRD_Channel then
|
||||
log.debug("received packet on unconfigured channel " .. l_chan, true)
|
||||
elseif r_chan == config.PKT_Channel then
|
||||
if not self.sv_linked then
|
||||
log.debug("discarding pocket API packet before linked to supervisor")
|
||||
elseif protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
-- look for an associated session
|
||||
local session = apisessions.find_session(r_port)
|
||||
local session = apisessions.find_session(src_addr)
|
||||
|
||||
-- API packet
|
||||
-- coordinator packet
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
else
|
||||
-- any other packet should be session related, discard it
|
||||
log.debug("discarding COORD_API packet without a known session")
|
||||
log.debug("discarding SCADA_CRDN packet without a known session")
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
-- look for an associated session
|
||||
local session = apisessions.find_session(r_port)
|
||||
local session = apisessions.find_session(src_addr)
|
||||
|
||||
-- SCADA management packet
|
||||
if session ~= nil then
|
||||
-- pass the packet onto the session handler
|
||||
session.in_queue.push_packet(packet)
|
||||
elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- establish a new session
|
||||
local next_seq_id = packet.scada_frame.seq_num() + 1
|
||||
|
||||
-- validate packet and continue
|
||||
if packet.length == 3 and type(packet.data[1]) == "string" and type(packet.data[2]) == "string" then
|
||||
local comms_v = packet.data[1]
|
||||
@@ -473,42 +460,42 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
local dev_type = packet.data[3]
|
||||
|
||||
if comms_v ~= comms.version then
|
||||
if self.last_api_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.last_api_est_acks[src_addr] ~= ESTABLISH_ACK.BAD_VERSION then
|
||||
log.info(util.c("dropping API establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
|
||||
self.last_api_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
|
||||
end
|
||||
|
||||
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.BAD_VERSION)
|
||||
elseif dev_type == DEVICE_TYPE.PKT then
|
||||
-- pocket linking request
|
||||
local id = apisessions.establish_session(l_port, r_port, firmware_v)
|
||||
println(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id))
|
||||
coordinator.log_comms(util.c("API: pocket (", firmware_v, ") [:", r_port, "] connected with session ID ", id))
|
||||
local id = apisessions.establish_session(src_addr, firmware_v)
|
||||
coordinator.log_comms(util.c("API_ESTABLISH: pocket (", firmware_v, ") [@", src_addr, "] connected with session ID ", id))
|
||||
|
||||
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
|
||||
self.last_api_est_acks[r_port] = ESTABLISH_ACK.ALLOW
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.ALLOW)
|
||||
else
|
||||
log.debug(util.c("illegal establish packet for device ", dev_type, " on API listening channel"))
|
||||
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
|
||||
log.debug(util.c("API_ESTABLISH: illegal establish packet for device ", dev_type, " on pocket channel"))
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
|
||||
end
|
||||
else
|
||||
log.debug("invalid establish packet (on API listening channel)")
|
||||
_send_api_establish_ack(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
|
||||
_send_api_establish_ack(packet.scada_frame, ESTABLISH_ACK.DENY)
|
||||
end
|
||||
else
|
||||
-- any other packet should be session related, discard it
|
||||
log.debug(util.c(r_port, "->", l_port, ": discarding SCADA_MGMT packet without a known session"))
|
||||
log.debug(util.c("discarding pocket SCADA_MGMT packet without a known session from computer ", src_addr))
|
||||
end
|
||||
else
|
||||
log.debug("illegal packet type " .. protocol .. " on api listening channel", true)
|
||||
log.debug("illegal packet type " .. protocol .. " on pocket channel", true)
|
||||
end
|
||||
elseif l_port == sv_listen then
|
||||
elseif r_chan == config.SVR_Channel then
|
||||
-- check sequence number
|
||||
if self.sv_r_seq_num == nil then
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
elseif self.connected and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||
elseif self.sv_linked and ((self.sv_r_seq_num + 1) ~= packet.scada_frame.seq_num()) then
|
||||
log.warning("sequence out-of-order: last = " .. self.sv_r_seq_num .. ", new = " .. packet.scada_frame.seq_num())
|
||||
return
|
||||
return false
|
||||
elseif self.sv_linked and src_addr ~= self.sv_addr then
|
||||
log.debug("received packet from unknown computer " .. src_addr .. " while linked; channel in use by another system?")
|
||||
return false
|
||||
else
|
||||
self.sv_r_seq_num = packet.scada_frame.seq_num()
|
||||
end
|
||||
@@ -520,7 +507,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
if protocol == PROTOCOL.SCADA_CRDN then
|
||||
---@cast packet crdn_frame
|
||||
if self.sv_linked then
|
||||
if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
|
||||
if packet.type == CRDN_TYPE.INITIAL_BUILDS then
|
||||
if packet.length == 2 then
|
||||
-- record builds
|
||||
local fac_builds = iocontrol.record_facility_builds(packet.data[1])
|
||||
@@ -528,31 +515,31 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
|
||||
if fac_builds and unit_builds then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.INITIAL_BUILDS, {})
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.INITIAL_BUILDS, {})
|
||||
else
|
||||
log.debug("received invalid INITIAL_BUILDS packet")
|
||||
end
|
||||
else
|
||||
log.debug("INITIAL_BUILDS packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_BUILDS then
|
||||
elseif packet.type == CRDN_TYPE.FAC_BUILDS then
|
||||
if packet.length == 1 then
|
||||
-- record facility builds
|
||||
if iocontrol.record_facility_builds(packet.data[1]) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_BUILDS, {})
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.FAC_BUILDS, {})
|
||||
else
|
||||
log.debug("received invalid FAC_BUILDS packet")
|
||||
end
|
||||
else
|
||||
log.debug("FAC_BUILDS packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_STATUS then
|
||||
elseif packet.type == CRDN_TYPE.FAC_STATUS then
|
||||
-- update facility status
|
||||
if not iocontrol.update_facility_status(packet.data) then
|
||||
log.debug("received invalid FAC_STATUS packet")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then
|
||||
elseif packet.type == CRDN_TYPE.FAC_CMD then
|
||||
-- facility command acknowledgement
|
||||
if packet.length >= 2 then
|
||||
local cmd = packet.data[1]
|
||||
@@ -570,30 +557,34 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
end
|
||||
elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
|
||||
iocontrol.get_db().facility.ack_alarms_ack(ack)
|
||||
elseif cmd == FAC_COMMAND.SET_WASTE_MODE then
|
||||
process.waste_ack_handle(packet.data[2])
|
||||
elseif cmd == FAC_COMMAND.SET_PU_FB then
|
||||
process.pu_fb_ack_handle(packet.data[2])
|
||||
else
|
||||
log.debug(util.c("received facility command ack with unknown command ", cmd))
|
||||
end
|
||||
else
|
||||
log.debug("SCADA_CRDN facility command ack packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_BUILDS then
|
||||
elseif packet.type == CRDN_TYPE.UNIT_BUILDS then
|
||||
-- record builds
|
||||
if packet.length == 1 then
|
||||
if iocontrol.record_unit_builds(packet.data[1]) then
|
||||
-- acknowledge receipt of builds
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_BUILDS, {})
|
||||
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.UNIT_BUILDS, {})
|
||||
else
|
||||
log.debug("received invalid UNIT_BUILDS packet")
|
||||
end
|
||||
else
|
||||
log.debug("UNIT_BUILDS packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then
|
||||
elseif packet.type == CRDN_TYPE.UNIT_STATUSES then
|
||||
-- update statuses
|
||||
if not iocontrol.update_unit_statuses(packet.data) then
|
||||
log.debug("received invalid UNIT_STATUSES packet")
|
||||
end
|
||||
elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then
|
||||
elseif packet.type == CRDN_TYPE.UNIT_CMD then
|
||||
-- unit command acknowledgement
|
||||
if packet.length == 3 then
|
||||
local cmd = packet.data[1]
|
||||
@@ -634,36 +625,68 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
end
|
||||
elseif protocol == PROTOCOL.SCADA_MGMT then
|
||||
---@cast packet mgmt_frame
|
||||
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
|
||||
if self.sv_linked then
|
||||
if packet.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 750 then
|
||||
log.warning("coordinator KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("coordinator RTT = " .. trip_time .. "ms")
|
||||
|
||||
iocontrol.get_db().facility.ps.publish("sv_ping", trip_time)
|
||||
|
||||
_send_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
self.sv_addr = comms.BROADCAST
|
||||
self.sv_linked = false
|
||||
self.sv_r_seq_num = nil
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
log.info("server connection closed by remote host")
|
||||
else
|
||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
|
||||
end
|
||||
elseif packet.type == MGMT_TYPE.ESTABLISH then
|
||||
-- connection with supervisor established
|
||||
if packet.length == 2 then
|
||||
local est_ack = packet.data[1]
|
||||
local config = packet.data[2]
|
||||
local sv_config = packet.data[2]
|
||||
|
||||
if est_ack == ESTABLISH_ACK.ALLOW then
|
||||
if type(config) == "table" and #config > 1 then
|
||||
-- reset to disconnected before validating
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
|
||||
if type(sv_config) == "table" and #sv_config == 2 then
|
||||
-- get configuration
|
||||
|
||||
---@class facility_conf
|
||||
local conf = {
|
||||
num_units = config[1], ---@type integer
|
||||
defs = {} -- boilers and turbines
|
||||
num_units = sv_config[1], ---@type integer
|
||||
cooling = sv_config[2] ---@type sv_cooling_conf
|
||||
}
|
||||
|
||||
if (#config - 1) == (conf.num_units * 2) then
|
||||
-- record sequence of pairs of [#boilers, #turbines] per unit
|
||||
for i = 2, #config do
|
||||
table.insert(conf.defs, config[i])
|
||||
end
|
||||
|
||||
if conf.num_units == config.UnitCount then
|
||||
-- init io controller
|
||||
iocontrol.init(conf, public)
|
||||
|
||||
self.sv_addr = src_addr
|
||||
self.sv_linked = true
|
||||
self.sv_r_seq_num = nil
|
||||
self.sv_config_err = false
|
||||
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.LINKED)
|
||||
else
|
||||
self.sv_config_err = true
|
||||
log.warning("invalid supervisor configuration definitions received, establish failed")
|
||||
log.warning("supervisor config's number of units don't match coordinator's config, establish failed")
|
||||
end
|
||||
else
|
||||
log.debug("invalid supervisor configuration table received, establish failed")
|
||||
@@ -678,14 +701,17 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
|
||||
if est_ack == ESTABLISH_ACK.DENY then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.DENIED)
|
||||
log.info("supervisor connection denied")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.COLLISION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.COLLISION)
|
||||
log.warning("supervisor connection denied due to collision")
|
||||
end
|
||||
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
|
||||
if self.last_est_ack ~= est_ack then
|
||||
iocontrol.fp_link_state(types.PANEL_LINK_STATE.BAD_VERSION)
|
||||
log.warning("supervisor comms version mismatch")
|
||||
end
|
||||
else
|
||||
@@ -696,34 +722,6 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
else
|
||||
log.debug("SCADA_MGMT establish packet length mismatch")
|
||||
end
|
||||
elseif self.sv_linked then
|
||||
if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive request received, echo back
|
||||
if packet.length == 1 then
|
||||
local timestamp = packet.data[1]
|
||||
local trip_time = util.time() - timestamp
|
||||
|
||||
if trip_time > 750 then
|
||||
log.warning("coord KEEP_ALIVE trip time > 750ms (" .. trip_time .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug("coord RTT = " .. trip_time .. "ms")
|
||||
|
||||
iocontrol.get_db().facility.ps.publish("sv_ping", trip_time)
|
||||
|
||||
_send_keep_alive_ack(timestamp)
|
||||
else
|
||||
log.debug("SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
-- handle session close
|
||||
sv_watchdog.cancel()
|
||||
self.sv_linked = false
|
||||
println_ts("server connection closed by remote host")
|
||||
log.info("server connection closed by remote host")
|
||||
else
|
||||
log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
|
||||
end
|
||||
else
|
||||
log.debug("discarding non-link SCADA_MGMT packet before linked")
|
||||
end
|
||||
@@ -731,9 +729,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
|
||||
log.debug("illegal packet type " .. protocol .. " on supervisor listening channel", true)
|
||||
end
|
||||
else
|
||||
log.debug("received packet on unconfigured channel " .. l_port, true)
|
||||
log.debug("received packet for unknown channel " .. r_chan, true)
|
||||
end
|
||||
end
|
||||
|
||||
return was_linked and not self.sv_linked
|
||||
end
|
||||
|
||||
-- check if the coordinator is still linked to the supervisor
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ local FAC_COMMAND = comms.FAC_COMMAND
|
||||
local UNIT_COMMAND = comms.UNIT_COMMAND
|
||||
|
||||
local PROCESS = types.PROCESS
|
||||
local PRODUCT = types.WASTE_PRODUCT
|
||||
|
||||
---@class process_controller
|
||||
local process = {}
|
||||
@@ -18,13 +19,20 @@ local process = {}
|
||||
local self = {
|
||||
io = nil, ---@type ioctl
|
||||
comms = nil, ---@type coord_comms
|
||||
---@class coord_auto_config
|
||||
config = {
|
||||
mode = PROCESS.INACTIVE,
|
||||
burn_target = 0.0,
|
||||
charge_target = 0.0,
|
||||
gen_target = 0.0,
|
||||
limits = {}
|
||||
---@class coord_control_states
|
||||
control_states = {
|
||||
---@class coord_auto_config
|
||||
process = {
|
||||
mode = PROCESS.INACTIVE,
|
||||
burn_target = 0.0,
|
||||
charge_target = 0.0,
|
||||
gen_target = 0.0,
|
||||
limits = {},
|
||||
waste_product = PRODUCT.PLUTONIUM,
|
||||
pu_fallback = false
|
||||
},
|
||||
waste_modes = {},
|
||||
priority_groups = {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,55 +47,64 @@ function process.init(iocontrol, coord_comms)
|
||||
self.io = iocontrol
|
||||
self.comms = coord_comms
|
||||
|
||||
local ctl_proc = self.control_states.process
|
||||
|
||||
for i = 1, self.io.facility.num_units do
|
||||
self.config.limits[i] = 0.1
|
||||
ctl_proc.limits[i] = 0.1
|
||||
end
|
||||
|
||||
-- load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.error("process.init(): failed to load coordinator settings file")
|
||||
end
|
||||
|
||||
local config = settings.get("PROCESS") ---@type coord_auto_config|nil
|
||||
local ctrl_states = settings.get("ControlStates", {})
|
||||
local config = ctrl_states.process ---@type coord_auto_config
|
||||
|
||||
-- facility auto control configuration
|
||||
if type(config) == "table" then
|
||||
self.config.mode = config.mode
|
||||
self.config.burn_target = config.burn_target
|
||||
self.config.charge_target = config.charge_target
|
||||
self.config.gen_target = config.gen_target
|
||||
self.config.limits = config.limits
|
||||
ctl_proc.mode = config.mode
|
||||
ctl_proc.burn_target = config.burn_target
|
||||
ctl_proc.charge_target = config.charge_target
|
||||
ctl_proc.gen_target = config.gen_target
|
||||
ctl_proc.limits = config.limits
|
||||
ctl_proc.waste_product = config.waste_product
|
||||
ctl_proc.pu_fallback = config.pu_fallback
|
||||
|
||||
self.io.facility.ps.publish("process_mode", self.config.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
|
||||
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
|
||||
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
|
||||
|
||||
for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do
|
||||
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do
|
||||
local unit = self.io.units[id] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", self.config.limits[id])
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[id])
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded auto control settings from coord.settings")
|
||||
log.info("PROCESS: loaded auto control settings")
|
||||
|
||||
-- notify supervisor of auto waste config
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
|
||||
end
|
||||
|
||||
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
|
||||
|
||||
if type(waste_mode) == "table" then
|
||||
for id, mode in pairs(waste_mode) do
|
||||
-- unit waste states
|
||||
local waste_modes = ctrl_states.waste_modes ---@type table|nil
|
||||
if type(waste_modes) == "table" then
|
||||
for id, mode in pairs(waste_modes) do
|
||||
self.control_states.waste_modes[id] = mode
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded waste mode settings from coord.settings")
|
||||
log.info("PROCESS: loaded unit waste mode settings")
|
||||
end
|
||||
|
||||
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
|
||||
|
||||
-- unit priority groups
|
||||
local prio_groups = ctrl_states.priority_groups ---@type table|nil
|
||||
if type(prio_groups) == "table" then
|
||||
for id, group in pairs(prio_groups) do
|
||||
self.control_states.priority_groups[id] = group
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
|
||||
end
|
||||
|
||||
log.info("PROCESS: loaded priority groups settings from coord.settings")
|
||||
log.info("PROCESS: loaded priority groups settings")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -137,23 +154,18 @@ end
|
||||
-- set waste mode
|
||||
---@param id integer unit ID
|
||||
---@param mode integer waste mode
|
||||
function process.set_waste(id, mode)
|
||||
function process.set_unit_waste(id, mode)
|
||||
-- publish so that if it fails then it gets reset
|
||||
self.io.units[id].unit_ps.publish("U_WasteMode", mode)
|
||||
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
|
||||
log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
|
||||
|
||||
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
|
||||
self.control_states.waste_modes[id] = mode
|
||||
settings.set("ControlStates", self.control_states)
|
||||
|
||||
if type(waste_mode) ~= "table" then waste_mode = {} end
|
||||
|
||||
waste_mode[id] = mode
|
||||
|
||||
settings.set("WASTE_MODES", waste_mode)
|
||||
|
||||
if not settings.save("/coord.settings") then
|
||||
log.error("process.set_waste(): failed to save coordinator settings file")
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_unit_waste(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -187,15 +199,10 @@ function process.set_group(unit_id, group_id)
|
||||
self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
|
||||
log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
|
||||
|
||||
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
|
||||
self.control_states.priority_groups[unit_id] = group_id
|
||||
settings.set("ControlStates", self.control_states)
|
||||
|
||||
if type(prio_groups) ~= "table" then prio_groups = {} end
|
||||
|
||||
prio_groups[unit_id] = group_id
|
||||
|
||||
settings.set("PRIORITY_GROUPS", prio_groups)
|
||||
|
||||
if not settings.save("/coord.settings") then
|
||||
if not settings.save("/coordinator.settings") then
|
||||
log.error("process.set_group(): failed to save coordinator settings file")
|
||||
end
|
||||
end
|
||||
@@ -204,6 +211,18 @@ end
|
||||
-- AUTO PROCESS CONTROL --
|
||||
--------------------------
|
||||
|
||||
-- write auto process control to config file
|
||||
local function _write_auto_config()
|
||||
-- save config
|
||||
settings.set("ControlStates", self.control_states)
|
||||
local saved = settings.save("/coordinator.settings")
|
||||
if not saved then
|
||||
log.warning("process._write_auto_config(): failed to save coordinator settings file")
|
||||
end
|
||||
|
||||
return saved
|
||||
end
|
||||
|
||||
-- stop automatic process control
|
||||
function process.stop_auto()
|
||||
self.comms.send_fac_command(FAC_COMMAND.STOP)
|
||||
@@ -212,10 +231,34 @@ end
|
||||
|
||||
-- start automatic process control
|
||||
function process.start_auto()
|
||||
self.comms.send_auto_start(self.config)
|
||||
self.comms.send_auto_start(self.control_states.process)
|
||||
log.debug("PROCESS: START AUTO CTL")
|
||||
end
|
||||
|
||||
-- set automatic process control waste mode
|
||||
---@param product WASTE_PRODUCT waste product for auto control
|
||||
function process.set_process_waste(product)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, product)
|
||||
|
||||
log.debug(util.c("PROCESS: SET WASTE ", product))
|
||||
|
||||
-- update config table and save
|
||||
self.control_states.process.waste_product = product
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- set automatic process control plutonium fallback
|
||||
---@param enabled boolean whether to enable plutonium fallback
|
||||
function process.set_pu_fallback(enabled)
|
||||
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, enabled)
|
||||
|
||||
log.debug(util.c("PROCESS: SET PU FALLBACK ", enabled))
|
||||
|
||||
-- update config table and save
|
||||
self.control_states.process.pu_fallback = enabled
|
||||
_write_auto_config()
|
||||
end
|
||||
|
||||
-- save process control settings
|
||||
---@param mode PROCESS control mode
|
||||
---@param burn_target number burn rate target
|
||||
@@ -223,29 +266,18 @@ end
|
||||
---@param gen_target number generation rate target
|
||||
---@param limits table unit burn rate limits
|
||||
function process.save(mode, burn_target, charge_target, gen_target, limits)
|
||||
-- attempt to load settings
|
||||
if not settings.load("/coord.settings") then
|
||||
log.warning("process.save(): failed to load coordinator settings file")
|
||||
end
|
||||
log.debug("PROCESS: SAVE")
|
||||
|
||||
-- config table
|
||||
self.config = {
|
||||
mode = mode,
|
||||
burn_target = burn_target,
|
||||
charge_target = charge_target,
|
||||
gen_target = gen_target,
|
||||
limits = limits
|
||||
}
|
||||
-- update config table
|
||||
local ctl_proc = self.control_states.process
|
||||
ctl_proc.mode = mode
|
||||
ctl_proc.burn_target = burn_target
|
||||
ctl_proc.charge_target = charge_target
|
||||
ctl_proc.gen_target = gen_target
|
||||
ctl_proc.limits = limits
|
||||
|
||||
-- save config
|
||||
settings.set("PROCESS", self.config)
|
||||
local saved = settings.save("/coord.settings")
|
||||
|
||||
if not saved then
|
||||
log.warning("process.save(): failed to save coordinator settings file")
|
||||
end
|
||||
|
||||
self.io.facility.save_cfg_ack(saved)
|
||||
self.io.facility.save_cfg_ack(_write_auto_config())
|
||||
end
|
||||
|
||||
-- handle a start command acknowledgement
|
||||
@@ -253,21 +285,39 @@ end
|
||||
function process.start_ack_handle(response)
|
||||
local ack = response[1]
|
||||
|
||||
self.config.mode = response[2]
|
||||
self.config.burn_target = response[3]
|
||||
self.config.charge_target = response[4]
|
||||
self.config.gen_target = response[5]
|
||||
local ctl_proc = self.control_states.process
|
||||
ctl_proc.mode = response[2]
|
||||
ctl_proc.burn_target = response[3]
|
||||
ctl_proc.charge_target = response[4]
|
||||
ctl_proc.gen_target = response[5]
|
||||
|
||||
for i = 1, #response[6] do
|
||||
self.config.limits[i] = response[6][i]
|
||||
for i = 1, math.min(#response[6], self.io.facility.num_units) do
|
||||
ctl_proc.limits[i] = response[6][i]
|
||||
|
||||
local unit = self.io.units[i] ---@type ioctl_unit
|
||||
unit.unit_ps.publish("burn_limit", ctl_proc.limits[i])
|
||||
end
|
||||
|
||||
self.io.facility.ps.publish("auto_mode", self.config.mode)
|
||||
self.io.facility.ps.publish("burn_target", self.config.burn_target)
|
||||
self.io.facility.ps.publish("charge_target", self.config.charge_target)
|
||||
self.io.facility.ps.publish("gen_target", self.config.gen_target)
|
||||
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
|
||||
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
|
||||
self.io.facility.ps.publish("process_charge_target", ctl_proc.charge_target)
|
||||
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
|
||||
|
||||
self.io.facility.start_ack(ack)
|
||||
end
|
||||
|
||||
-- record waste product state after attempting to change it
|
||||
---@param response WASTE_PRODUCT supervisor waste product state
|
||||
function process.waste_ack_handle(response)
|
||||
self.control_states.process.waste_product = response
|
||||
self.io.facility.ps.publish("process_waste_product", response)
|
||||
end
|
||||
|
||||
-- record plutonium fallback state after attempting to change it
|
||||
---@param response boolean supervisor plutonium fallback state
|
||||
function process.pu_fb_ack_handle(response)
|
||||
self.control_states.process.pu_fallback = response
|
||||
self.io.facility.ps.publish("process_pu_fallback", response)
|
||||
end
|
||||
|
||||
return process
|
||||
|
||||
@@ -3,17 +3,23 @@
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
|
||||
local flow_view = require("coordinator.ui.layout.flow_view")
|
||||
local panel_view = require("coordinator.ui.layout.front_panel")
|
||||
local main_view = require("coordinator.ui.layout.main_view")
|
||||
local unit_view = require("coordinator.ui.layout.unit_view")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local flasher = require("graphics.flasher")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
|
||||
---@class coord_renderer
|
||||
local renderer = {}
|
||||
|
||||
-- render engine
|
||||
@@ -21,10 +27,14 @@ local engine = {
|
||||
monitors = nil, ---@type monitors_struct|nil
|
||||
dmesg_window = nil, ---@type table|nil
|
||||
ui_ready = false,
|
||||
fp_ready = false,
|
||||
ui = {
|
||||
front_panel = nil, ---@type graphics_element|nil
|
||||
main_display = nil, ---@type graphics_element|nil
|
||||
flow_display = nil, ---@type graphics_element|nil
|
||||
unit_displays = {}
|
||||
}
|
||||
},
|
||||
disable_flow_view = false
|
||||
}
|
||||
|
||||
-- init a display to the "default", but set text scale to 0.5
|
||||
@@ -42,64 +52,44 @@ local function _init_display(monitor)
|
||||
end
|
||||
end
|
||||
|
||||
-- disable the flow view
|
||||
---@param disable boolean
|
||||
function renderer.legacy_disable_flow_view(disable)
|
||||
engine.disable_flow_view = disable
|
||||
end
|
||||
|
||||
-- link to the monitor peripherals
|
||||
---@param monitors monitors_struct
|
||||
function renderer.set_displays(monitors)
|
||||
engine.monitors = monitors
|
||||
end
|
||||
|
||||
-- check if the renderer is configured to use a given monitor peripheral
|
||||
---@nodiscard
|
||||
---@param periph table peripheral
|
||||
---@return boolean is_used
|
||||
function renderer.is_monitor_used(periph)
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary == periph then
|
||||
return true
|
||||
else
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
||||
if monitor == periph then return true end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
-- report to front panel as connected
|
||||
iocontrol.fp_monitor_state("main", engine.monitors.primary ~= nil)
|
||||
iocontrol.fp_monitor_state("flow", engine.monitors.flow ~= nil)
|
||||
for i = 1, #engine.monitors.unit_displays do iocontrol.fp_monitor_state(i, true) end
|
||||
end
|
||||
|
||||
-- init all displays in use by the renderer
|
||||
function renderer.init_displays()
|
||||
-- init primary monitor
|
||||
-- init primary and flow monitors
|
||||
_init_display(engine.monitors.primary)
|
||||
if not engine.disable_flow_view then _init_display(engine.monitors.flow) end
|
||||
|
||||
-- init unit displays
|
||||
for _, monitor in ipairs(engine.monitors.unit_displays) do
|
||||
_init_display(monitor)
|
||||
end
|
||||
end
|
||||
|
||||
-- check main display width
|
||||
---@nodiscard
|
||||
---@return boolean width_okay
|
||||
function renderer.validate_main_display_width()
|
||||
local w, _ = engine.monitors.primary.getSize()
|
||||
return w == 164
|
||||
end
|
||||
-- init terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
|
||||
-- check display sizes
|
||||
---@nodiscard
|
||||
---@return boolean valid all unit display dimensions OK
|
||||
function renderer.validate_unit_display_sizes()
|
||||
local valid = true
|
||||
|
||||
for id, monitor in ipairs(engine.monitors.unit_displays) do
|
||||
local w, h = monitor.getSize()
|
||||
if w ~= 79 or h ~= 52 then
|
||||
log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
|
||||
valid = false
|
||||
end
|
||||
-- set overridden colors
|
||||
for i = 1, #style.fp.colors do
|
||||
term.setPaletteColor(style.fp.colors[i].c, style.fp.colors[i].hex)
|
||||
end
|
||||
|
||||
return valid
|
||||
end
|
||||
|
||||
-- initialize the dmesg output window
|
||||
@@ -109,44 +99,123 @@ function renderer.init_dmesg()
|
||||
log.direct_dmesg(engine.dmesg_window)
|
||||
end
|
||||
|
||||
-- start the coordinator GUI
|
||||
function renderer.start_ui()
|
||||
-- try to start the front panel
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_fp()
|
||||
local status, msg = true, nil
|
||||
|
||||
if not engine.fp_ready then
|
||||
-- show front panel view on terminal
|
||||
status, msg = pcall(function ()
|
||||
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
|
||||
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
|
||||
end)
|
||||
|
||||
if status then
|
||||
-- start flasher callback task and report ready
|
||||
flasher.run()
|
||||
engine.fp_ready = true
|
||||
else
|
||||
-- report fail and close front panel
|
||||
msg = core.extract_assert_msg(msg)
|
||||
renderer.close_fp()
|
||||
end
|
||||
end
|
||||
|
||||
return status, msg
|
||||
end
|
||||
|
||||
-- close out the front panel
|
||||
function renderer.close_fp()
|
||||
if engine.fp_ready then
|
||||
if not engine.ui_ready then
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
end
|
||||
|
||||
-- disable PGI
|
||||
pgi.unlink()
|
||||
|
||||
-- hide to stop animation callbacks and clear root UI elements
|
||||
engine.ui.front_panel.hide()
|
||||
engine.ui.front_panel = nil
|
||||
engine.fp_ready = false
|
||||
|
||||
-- restore colors
|
||||
for i = 1, #style.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.colors[i].c)
|
||||
term.setPaletteColor(style.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
-- reset terminal
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- try to start the main GUI
|
||||
---@return boolean success, any error_msg
|
||||
function renderer.try_start_ui()
|
||||
local status, msg = true, nil
|
||||
|
||||
if not engine.ui_ready then
|
||||
-- hide dmesg
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
-- show main view on main monitor
|
||||
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
status, msg = pcall(function ()
|
||||
-- show main view on main monitor
|
||||
if engine.monitors.primary ~= nil then
|
||||
engine.ui.main_display = DisplayBox{window=engine.monitors.primary,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
end
|
||||
|
||||
-- show unit views on unit displays
|
||||
for i = 1, #engine.monitors.unit_displays do
|
||||
engine.ui.unit_displays[i] = DisplayBox{window=engine.monitors.unit_displays[i],fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[i], i)
|
||||
-- show flow view on flow monitor
|
||||
if engine.monitors.flow ~= nil then
|
||||
engine.ui.flow_display = DisplayBox{window=engine.monitors.flow,fg_bg=style.root}
|
||||
flow_view(engine.ui.flow_display)
|
||||
end
|
||||
|
||||
-- show unit views on unit displays
|
||||
for idx, display in pairs(engine.monitors.unit_displays) do
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=display,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
end
|
||||
end)
|
||||
|
||||
if status then
|
||||
-- start flasher callback task and report ready
|
||||
flasher.run()
|
||||
engine.ui_ready = true
|
||||
else
|
||||
-- report fail and close ui
|
||||
msg = core.extract_assert_msg(msg)
|
||||
renderer.close_ui()
|
||||
end
|
||||
|
||||
-- start flasher callback task
|
||||
flasher.run()
|
||||
|
||||
-- report ui as ready
|
||||
engine.ui_ready = true
|
||||
end
|
||||
|
||||
return status, msg
|
||||
end
|
||||
|
||||
-- close out the UI
|
||||
function renderer.close_ui()
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
if not engine.fp_ready then
|
||||
-- stop blinking indicators
|
||||
flasher.clear()
|
||||
end
|
||||
|
||||
-- delete element trees
|
||||
if engine.ui.main_display ~= nil then engine.ui.main_display.delete() end
|
||||
for _, display in ipairs(engine.ui.unit_displays) do display.delete() end
|
||||
if engine.ui.flow_display ~= nil then engine.ui.flow_display.delete() end
|
||||
for _, display in pairs(engine.ui.unit_displays) do display.delete() end
|
||||
|
||||
-- report ui as not ready
|
||||
engine.ui_ready = false
|
||||
|
||||
-- clear root UI elements
|
||||
engine.ui.main_display = nil
|
||||
engine.ui.flow_display = nil
|
||||
engine.ui.unit_displays = {}
|
||||
|
||||
-- clear unit monitors
|
||||
@@ -157,22 +226,145 @@ function renderer.close_ui()
|
||||
engine.dmesg_window.redraw()
|
||||
end
|
||||
|
||||
-- is the front panel ready?
|
||||
---@nodiscard
|
||||
---@return boolean ready
|
||||
function renderer.fp_ready() return engine.fp_ready end
|
||||
|
||||
-- is the UI ready?
|
||||
---@nodiscard
|
||||
---@return boolean ready
|
||||
function renderer.ui_ready() return engine.ui_ready end
|
||||
|
||||
-- handle a monitor peripheral being disconnected
|
||||
---@param device table monitor
|
||||
---@return boolean is_used if the monitor is one of the configured monitors
|
||||
function renderer.handle_disconnect(device)
|
||||
local is_used = false
|
||||
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary == device then
|
||||
if engine.ui.main_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.main_display.delete()
|
||||
end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.primary = nil
|
||||
engine.ui.main_display = nil
|
||||
|
||||
iocontrol.fp_monitor_state("main", false)
|
||||
elseif engine.monitors.flow == device then
|
||||
if engine.ui.flow_display ~= nil then
|
||||
-- delete element tree and clear root UI elements
|
||||
engine.ui.flow_display.delete()
|
||||
end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.flow = nil
|
||||
engine.ui.flow_display = nil
|
||||
|
||||
iocontrol.fp_monitor_state("flow", false)
|
||||
else
|
||||
for idx, monitor in pairs(engine.monitors.unit_displays) do
|
||||
if monitor == device then
|
||||
if engine.ui.unit_displays[idx] ~= nil then
|
||||
engine.ui.unit_displays[idx].delete()
|
||||
end
|
||||
|
||||
is_used = true
|
||||
engine.monitors.unit_displays[idx] = nil
|
||||
engine.ui.unit_displays[idx] = nil
|
||||
|
||||
iocontrol.fp_monitor_state(idx, false)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return is_used
|
||||
end
|
||||
|
||||
-- handle a monitor peripheral being reconnected
|
||||
---@param name string monitor name
|
||||
---@param device table monitor
|
||||
---@return boolean is_used if the monitor is one of the configured monitors
|
||||
function renderer.handle_reconnect(name, device)
|
||||
local is_used = false
|
||||
|
||||
if engine.monitors ~= nil then
|
||||
if engine.monitors.primary_name == name then
|
||||
is_used = true
|
||||
_init_display(device)
|
||||
engine.monitors.primary = device
|
||||
|
||||
local disp_x, disp_y = engine.monitors.primary.getSize()
|
||||
engine.dmesg_window.reposition(1, 1, disp_x, disp_y, engine.monitors.primary)
|
||||
|
||||
if engine.ui_ready and (engine.ui.main_display == nil) then
|
||||
engine.dmesg_window.setVisible(false)
|
||||
|
||||
engine.ui.main_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
main_view(engine.ui.main_display)
|
||||
else
|
||||
engine.dmesg_window.setVisible(true)
|
||||
engine.dmesg_window.redraw()
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("main", true)
|
||||
elseif engine.monitors.flow_name == name then
|
||||
is_used = true
|
||||
_init_display(device)
|
||||
engine.monitors.flow = device
|
||||
|
||||
if engine.ui_ready and (engine.ui.flow_display == nil) then
|
||||
engine.ui.flow_display = DisplayBox{window=device,fg_bg=style.root}
|
||||
flow_view(engine.ui.flow_display)
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state("flow", true)
|
||||
else
|
||||
for idx, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if monitor == name then
|
||||
is_used = true
|
||||
_init_display(device)
|
||||
engine.monitors.unit_displays[idx] = device
|
||||
|
||||
if engine.ui_ready and (engine.ui.unit_displays[idx] == nil) then
|
||||
engine.ui.unit_displays[idx] = DisplayBox{window=device,fg_bg=style.root}
|
||||
unit_view(engine.ui.unit_displays[idx], idx)
|
||||
end
|
||||
|
||||
iocontrol.fp_monitor_state(idx, true)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return is_used
|
||||
end
|
||||
|
||||
|
||||
-- handle a touch event
|
||||
---@param event mouse_interaction|nil
|
||||
function renderer.handle_mouse(event)
|
||||
if engine.ui_ready and event ~= nil then
|
||||
if event.monitor == engine.monitors.primary_name then
|
||||
engine.ui.main_display.handle_mouse(event)
|
||||
else
|
||||
for id, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if event.monitor == monitor then
|
||||
local layout = engine.ui.unit_displays[id] ---@type graphics_element
|
||||
layout.handle_mouse(event)
|
||||
if event ~= nil then
|
||||
if engine.fp_ready and event.monitor == "terminal" then
|
||||
engine.ui.front_panel.handle_mouse(event)
|
||||
elseif engine.ui_ready then
|
||||
if event.monitor == engine.monitors.primary_name then
|
||||
engine.ui.main_display.handle_mouse(event)
|
||||
elseif event.monitor == engine.monitors.flow_name then
|
||||
engine.ui.flow_display.handle_mouse(event)
|
||||
else
|
||||
for id, monitor in ipairs(engine.monitors.unit_name_map) do
|
||||
if event.monitor == monitor then
|
||||
local layout = engine.ui.unit_displays[id] ---@type graphics_element
|
||||
layout.handle_mouse(event)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local config = require("coordinator.config")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local api = require("coordinator.session.api")
|
||||
local pocket = require("coordinator.session.pocket")
|
||||
|
||||
local apisessions = {}
|
||||
|
||||
local self = {
|
||||
modem = nil,
|
||||
nic = nil, ---@type nic
|
||||
config = nil, ---@type crd_config
|
||||
next_id = 0,
|
||||
sessions = {}
|
||||
}
|
||||
@@ -18,7 +19,7 @@ local self = {
|
||||
-- PRIVATE FUNCTIONS --
|
||||
|
||||
-- handle a session output queue
|
||||
---@param session api_session_struct
|
||||
---@param session pkt_session_struct
|
||||
local function _api_handle_outq(session)
|
||||
-- record handler start time
|
||||
local handle_start = util.time()
|
||||
@@ -31,7 +32,7 @@ local function _api_handle_outq(session)
|
||||
if msg ~= nil then
|
||||
if msg.qtype == mqueue.TYPE.PACKET then
|
||||
-- handle a packet to be sent
|
||||
self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable())
|
||||
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||
elseif msg.qtype == mqueue.TYPE.COMMAND then
|
||||
-- handle instruction/notification
|
||||
elseif msg.qtype == mqueue.TYPE.DATA then
|
||||
@@ -41,15 +42,15 @@ local function _api_handle_outq(session)
|
||||
|
||||
-- max 100ms spent processing queue
|
||||
if util.time() - handle_start > 100 then
|
||||
log.warning("API out queue handler exceeded 100ms queue process limit")
|
||||
log.warning(util.c("offending session: port ", session.r_port))
|
||||
log.warning("API: out queue handler exceeded 100ms queue process limit")
|
||||
log.warning(util.c("API: offending session: ", session))
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- cleanly close a session
|
||||
---@param session api_session_struct
|
||||
---@param session pkt_session_struct
|
||||
local function _shutdown(session)
|
||||
session.open = false
|
||||
session.instance.close()
|
||||
@@ -58,77 +59,80 @@ local function _shutdown(session)
|
||||
while session.out_queue.ready() do
|
||||
local msg = session.out_queue.pop()
|
||||
if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then
|
||||
self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable())
|
||||
self.nic.transmit(self.config.PKT_Channel, self.config.CRD_Channel, msg.message)
|
||||
end
|
||||
end
|
||||
|
||||
log.debug(util.c("closed API session ", session.instance.get_id(), " on remote port ", session.r_port))
|
||||
log.debug(util.c("API: closed session ", session))
|
||||
end
|
||||
|
||||
-- PUBLIC FUNCTIONS --
|
||||
|
||||
-- initialize apisessions
|
||||
---@param modem table
|
||||
function apisessions.init(modem)
|
||||
self.modem = modem
|
||||
end
|
||||
|
||||
-- re-link the modem
|
||||
---@param modem table
|
||||
function apisessions.relink_modem(modem)
|
||||
self.modem = modem
|
||||
---@param nic nic network interface
|
||||
---@param config crd_config coordinator config
|
||||
function apisessions.init(nic, config)
|
||||
self.nic = nic
|
||||
self.config = config
|
||||
end
|
||||
|
||||
-- find a session by remote port
|
||||
---@nodiscard
|
||||
---@param port integer
|
||||
---@return api_session_struct|nil
|
||||
function apisessions.find_session(port)
|
||||
---@param source_addr integer
|
||||
---@return pkt_session_struct|nil
|
||||
function apisessions.find_session(source_addr)
|
||||
for i = 1, #self.sessions do
|
||||
if self.sessions[i].r_port == port then return self.sessions[i] end
|
||||
if self.sessions[i].s_addr == source_addr then return self.sessions[i] end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- establish a new API session
|
||||
---@nodiscard
|
||||
---@param local_port integer
|
||||
---@param remote_port integer
|
||||
---@param source_addr integer
|
||||
---@param version string
|
||||
---@return integer session_id
|
||||
function apisessions.establish_session(local_port, remote_port, version)
|
||||
---@class api_session_struct
|
||||
local api_s = {
|
||||
function apisessions.establish_session(source_addr, version)
|
||||
---@class pkt_session_struct
|
||||
local pkt_s = {
|
||||
open = true,
|
||||
version = version,
|
||||
l_port = local_port,
|
||||
r_port = remote_port,
|
||||
s_addr = source_addr,
|
||||
in_queue = mqueue.new(),
|
||||
out_queue = mqueue.new(),
|
||||
instance = nil ---@type api_session
|
||||
instance = nil ---@type pkt_session
|
||||
}
|
||||
|
||||
api_s.instance = api.new_session(self.next_id, api_s.in_queue, api_s.out_queue, config.API_TIMEOUT)
|
||||
table.insert(self.sessions, api_s)
|
||||
local id = self.next_id
|
||||
|
||||
log.debug(util.c("established new API session to ", remote_port, " with ID ", self.next_id))
|
||||
pkt_s.instance = pocket.new_session(id, source_addr, pkt_s.in_queue, pkt_s.out_queue, self.config.API_Timeout)
|
||||
table.insert(self.sessions, pkt_s)
|
||||
|
||||
self.next_id = self.next_id + 1
|
||||
local mt = {
|
||||
---@param s pkt_session_struct
|
||||
__tostring = function (s) return util.c("PKT [", id, "] (@", s.s_addr, ")") end
|
||||
}
|
||||
|
||||
setmetatable(pkt_s, mt)
|
||||
|
||||
iocontrol.fp_pkt_connected(id, version, source_addr)
|
||||
log.debug(util.c("API: established new session: ", pkt_s))
|
||||
|
||||
self.next_id = id + 1
|
||||
|
||||
-- success
|
||||
return api_s.instance.get_id()
|
||||
return pkt_s.instance.get_id()
|
||||
end
|
||||
|
||||
-- attempt to identify which session's watchdog timer fired
|
||||
---@param timer_event number
|
||||
function apisessions.check_all_watchdogs(timer_event)
|
||||
for i = 1, #self.sessions do
|
||||
local session = self.sessions[i] ---@type api_session_struct
|
||||
local session = self.sessions[i] ---@type pkt_session_struct
|
||||
if session.open then
|
||||
local triggered = session.instance.check_wd(timer_event)
|
||||
if triggered then
|
||||
log.debug(util.c("watchdog closing API session ", session.instance.get_id(),
|
||||
" on remote port ", session.r_port, "..."))
|
||||
log.debug(util.c("API: watchdog closing session ", session, "..."))
|
||||
_shutdown(session)
|
||||
end
|
||||
end
|
||||
@@ -138,7 +142,7 @@ end
|
||||
-- iterate all the API sessions
|
||||
function apisessions.iterate_all()
|
||||
for i = 1, #self.sessions do
|
||||
local session = self.sessions[i] ---@type api_session_struct
|
||||
local session = self.sessions[i] ---@type pkt_session_struct
|
||||
|
||||
if session.open and session.instance.iterate() then
|
||||
_api_handle_outq(session)
|
||||
@@ -152,10 +156,9 @@ end
|
||||
function apisessions.free_all_closed()
|
||||
local f = function (session) return session.open end
|
||||
|
||||
---@param session api_session_struct
|
||||
---@param session pkt_session_struct
|
||||
local on_delete = function (session)
|
||||
log.debug(util.c("free'ing closed API session ", session.instance.get_id(),
|
||||
" on remote port ", session.r_port))
|
||||
log.debug(util.c("API: free'ing closed session ", session))
|
||||
end
|
||||
|
||||
util.filter_table(self.sessions, f, on_delete)
|
||||
@@ -164,7 +167,7 @@ end
|
||||
-- close all open connections
|
||||
function apisessions.close_all()
|
||||
for i = 1, #self.sessions do
|
||||
local session = self.sessions[i] ---@type api_session_struct
|
||||
local session = self.sessions[i] ---@type pkt_session_struct
|
||||
if session.open then _shutdown(session) end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
local comms = require("scada-common.comms")
|
||||
local log = require("scada-common.log")
|
||||
local mqueue = require("scada-common.mqueue")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local api = {}
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pocket = {}
|
||||
|
||||
local PROTOCOL = comms.PROTOCOL
|
||||
-- local CAPI_TYPE = comms.CAPI_TYPE
|
||||
local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
|
||||
|
||||
local println = util.println
|
||||
-- local CRDN_TYPE = comms.CRDN_TYPE
|
||||
local MGMT_TYPE = comms.MGMT_TYPE
|
||||
|
||||
-- retry time constants in ms
|
||||
-- local INITIAL_WAIT = 1500
|
||||
@@ -21,8 +21,8 @@ local API_S_CMDS = {
|
||||
local API_S_DATA = {
|
||||
}
|
||||
|
||||
api.API_S_CMDS = API_S_CMDS
|
||||
api.API_S_DATA = API_S_DATA
|
||||
pocket.API_S_CMDS = API_S_CMDS
|
||||
pocket.API_S_DATA = API_S_DATA
|
||||
|
||||
local PERIODICS = {
|
||||
KEEP_ALIVE = 2000
|
||||
@@ -31,11 +31,12 @@ local PERIODICS = {
|
||||
-- pocket API session
|
||||
---@nodiscard
|
||||
---@param id integer session ID
|
||||
---@param s_addr integer device source address
|
||||
---@param in_queue mqueue in message queue
|
||||
---@param out_queue mqueue out message queue
|
||||
---@param timeout number communications timeout
|
||||
function api.new_session(id, in_queue, out_queue, timeout)
|
||||
local log_header = "api_session(" .. id .. "): "
|
||||
function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
|
||||
local log_header = "pkt_session(" .. id .. "): "
|
||||
|
||||
local self = {
|
||||
-- connection properties
|
||||
@@ -61,45 +62,46 @@ function api.new_session(id, in_queue, out_queue, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
---@class api_session
|
||||
---@class pkt_session
|
||||
local public = {}
|
||||
|
||||
-- mark this API session as closed, stop watchdog
|
||||
-- mark this pocket session as closed, stop watchdog
|
||||
local function _close()
|
||||
self.conn_watchdog.cancel()
|
||||
self.connected = false
|
||||
iocontrol.fp_pkt_disconnected(id)
|
||||
end
|
||||
|
||||
-- send a CAPI packet
|
||||
-----@param msg_type CAPI_TYPE
|
||||
-- send a CRDN packet
|
||||
-----@param msg_type CRDN_TYPE
|
||||
-----@param msg table
|
||||
-- local function _send(msg_type, msg)
|
||||
-- local s_pkt = comms.scada_packet()
|
||||
-- local c_pkt = comms.capi_packet()
|
||||
-- local c_pkt = comms.crdn_packet()
|
||||
|
||||
-- c_pkt.make(msg_type, msg)
|
||||
-- s_pkt.make(self.seq_num, PROTOCOL.COORD_API, c_pkt.raw_sendable())
|
||||
-- s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
|
||||
|
||||
-- out_queue.push_packet(s_pkt)
|
||||
-- self.seq_num = self.seq_num + 1
|
||||
-- end
|
||||
|
||||
-- send a SCADA management packet
|
||||
---@param msg_type SCADA_MGMT_TYPE
|
||||
---@param msg_type MGMT_TYPE
|
||||
---@param msg table
|
||||
local function _send_mgmt(msg_type, msg)
|
||||
local s_pkt = comms.scada_packet()
|
||||
local m_pkt = comms.mgmt_packet()
|
||||
|
||||
m_pkt.make(msg_type, msg)
|
||||
s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
s_pkt.make(s_addr, self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
|
||||
|
||||
out_queue.push_packet(s_pkt)
|
||||
self.seq_num = self.seq_num + 1
|
||||
end
|
||||
|
||||
-- handle a packet
|
||||
---@param pkt mgmt_frame|capi_frame
|
||||
---@param pkt mgmt_frame|crdn_frame
|
||||
local function _handle_packet(pkt)
|
||||
-- check sequence number
|
||||
if self.r_seq_num == nil then
|
||||
@@ -115,19 +117,17 @@ function api.new_session(id, in_queue, out_queue, timeout)
|
||||
self.conn_watchdog.feed()
|
||||
|
||||
-- process packet
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.COORD_API then
|
||||
---@cast pkt capi_frame
|
||||
-- feed watchdog
|
||||
self.conn_watchdog.feed()
|
||||
if pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
|
||||
---@cast pkt crdn_frame
|
||||
|
||||
-- handle packet by type
|
||||
if pkt.type == nil then
|
||||
else
|
||||
log.debug(log_header .. "handler received unsupported CAPI packet type " .. pkt.type)
|
||||
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
|
||||
end
|
||||
elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
|
||||
---@cast pkt mgmt_frame
|
||||
if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
|
||||
if pkt.type == MGMT_TYPE.KEEP_ALIVE then
|
||||
-- keep alive reply
|
||||
if pkt.length == 2 then
|
||||
local srv_start = pkt.data[1]
|
||||
@@ -136,15 +136,17 @@ function api.new_session(id, in_queue, out_queue, timeout)
|
||||
self.last_rtt = srv_now - srv_start
|
||||
|
||||
if self.last_rtt > 750 then
|
||||
log.warning(log_header .. "API KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
log.warning(log_header .. "PKT KEEP_ALIVE round trip time > 750ms (" .. self.last_rtt .. "ms)")
|
||||
end
|
||||
|
||||
-- log.debug(log_header .. "API RTT = " .. self.last_rtt .. "ms")
|
||||
-- log.debug(log_header .. "API TT = " .. (srv_now - api_send) .. "ms")
|
||||
-- log.debug(log_header .. "PKT RTT = " .. self.last_rtt .. "ms")
|
||||
-- log.debug(log_header .. "PKT TT = " .. (srv_now - api_send) .. "ms")
|
||||
|
||||
iocontrol.fp_pkt_rtt(id, self.last_rtt)
|
||||
else
|
||||
log.debug(log_header .. "SCADA keep alive packet length mismatch")
|
||||
end
|
||||
elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
|
||||
elseif pkt.type == MGMT_TYPE.CLOSE then
|
||||
-- close the session
|
||||
_close()
|
||||
else
|
||||
@@ -172,8 +174,7 @@ function api.new_session(id, in_queue, out_queue, timeout)
|
||||
-- close the connection
|
||||
function public.close()
|
||||
_close()
|
||||
_send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
|
||||
println("connection to API session " .. id .. " closed by server")
|
||||
_send_mgmt(MGMT_TYPE.CLOSE, {})
|
||||
log.info(log_header .. "session closed by server")
|
||||
end
|
||||
|
||||
@@ -212,7 +213,6 @@ function api.new_session(id, in_queue, out_queue, timeout)
|
||||
|
||||
-- exit if connection was closed
|
||||
if not self.connected then
|
||||
println("connection to API session " .. id .. " closed by remote host")
|
||||
log.info(log_header .. "session closed by remote host")
|
||||
return self.connected
|
||||
end
|
||||
@@ -229,7 +229,7 @@ function api.new_session(id, in_queue, out_queue, timeout)
|
||||
|
||||
periodics.keep_alive = periodics.keep_alive + elapsed
|
||||
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
|
||||
_send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||
_send_mgmt(MGMT_TYPE.KEEP_ALIVE, { util.time() })
|
||||
periodics.keep_alive = 0
|
||||
end
|
||||
|
||||
@@ -248,4 +248,4 @@ function api.new_session(id, in_queue, out_queue, timeout)
|
||||
return public
|
||||
end
|
||||
|
||||
return api
|
||||
return pocket
|
||||
@@ -2,269 +2,25 @@
|
||||
-- Alarm Sounder
|
||||
--
|
||||
|
||||
local audio = require("scada-common.audio")
|
||||
local log = require("scada-common.log")
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local ALARM = types.ALARM
|
||||
local ALARM_STATE = types.ALARM_STATE
|
||||
|
||||
---@class sounder
|
||||
local sounder = {}
|
||||
|
||||
-- note: max samples = 0x20000 (128 * 1024 samples)
|
||||
|
||||
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
|
||||
local _DRATE = 48000 -- 48kHz audio
|
||||
local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
|
||||
local _05s_SAMPLES = 24000 -- half a second worth of samples
|
||||
|
||||
local test_alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||
|
||||
local alarm_ctl = {
|
||||
speaker = nil,
|
||||
volume = 0.5,
|
||||
playing = false,
|
||||
num_active = 0,
|
||||
next_block = 1,
|
||||
-- split audio up into 0.5s samples so specific components can be ended quicker
|
||||
quad_buffer = { {}, {}, {}, {} }
|
||||
stream = audio.new_stream()
|
||||
}
|
||||
|
||||
-- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones
|
||||
|
||||
local T_340Hz_Int_2Hz = 1
|
||||
local T_544Hz_440Hz_Alt = 2
|
||||
local T_660Hz_Int_125ms = 3
|
||||
local T_745Hz_Int_1Hz = 4
|
||||
local T_800Hz_Int = 5
|
||||
local T_800Hz_1000Hz_Alt = 6
|
||||
local T_1000Hz_Int = 7
|
||||
local T_1800Hz_Int_4Hz = 8
|
||||
|
||||
local TONES = {
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 340Hz @ 2Hz Intermittent
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 544Hz 100mS / 440Hz 400mS Alternating
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 660Hz @ 125ms On 125ms Off
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 745Hz @ 1Hz Intermittent
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 800Hz @ 0.25s On 1.75s Off
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 800/1000Hz @ 0.25s Alternating
|
||||
{ active = false, component = { {}, {}, {}, {} } }, -- 1KHz 1s on, 1s off Intermittent
|
||||
{ active = false, component = { {}, {}, {}, {} } } -- 1.8KHz @ 4Hz Intermittent
|
||||
}
|
||||
|
||||
-- calculate how many samples are in the given number of milliseconds
|
||||
---@nodiscard
|
||||
---@param ms integer milliseconds
|
||||
---@return integer samples
|
||||
local function ms_to_samples(ms) return math.floor(ms * 48) end
|
||||
|
||||
--#region Tone Generation (the Maths)
|
||||
|
||||
-- 340Hz @ 2Hz Intermittent
|
||||
local function gen_tone_1()
|
||||
local t, dt = 0, _2_PI * 340 / _DRATE
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[1].component[1][i] = val
|
||||
TONES[1].component[3][i] = val
|
||||
TONES[1].component[2][i] = 0
|
||||
TONES[1].component[4][i] = 0
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 544Hz 100mS / 440Hz 400mS Alternating
|
||||
local function gen_tone_2()
|
||||
local t1, dt1 = 0, _2_PI * 544 / _DRATE
|
||||
local t2, dt2 = 0, _2_PI * 440 / _DRATE
|
||||
local alternate_at = ms_to_samples(100)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local value
|
||||
|
||||
if i <= alternate_at then
|
||||
value = math.floor(math.sin(t1) * _MAX_VAL)
|
||||
t1 = (t1 + dt1) % _2_PI
|
||||
else
|
||||
value = math.floor(math.sin(t2) * _MAX_VAL)
|
||||
t2 = (t2 + dt2) % _2_PI
|
||||
end
|
||||
|
||||
TONES[2].component[1][i] = value
|
||||
TONES[2].component[2][i] = value
|
||||
TONES[2].component[3][i] = value
|
||||
TONES[2].component[4][i] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- 660Hz @ 125ms On 125ms Off
|
||||
local function gen_tone_3()
|
||||
local elapsed_samples = 0
|
||||
local alternate_after = ms_to_samples(125)
|
||||
local alternate_at = alternate_after
|
||||
local mode = true
|
||||
|
||||
local t, dt = 0, _2_PI * 660 / _DRATE
|
||||
|
||||
for set = 1, 4 do
|
||||
for i = 1, _05s_SAMPLES do
|
||||
if mode then
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[3].component[set][i] = val
|
||||
t = (t + dt) % _2_PI
|
||||
else
|
||||
t = 0
|
||||
TONES[3].component[set][i] = 0
|
||||
end
|
||||
|
||||
if elapsed_samples == alternate_at then
|
||||
mode = not mode
|
||||
alternate_at = elapsed_samples + alternate_after
|
||||
end
|
||||
|
||||
elapsed_samples = elapsed_samples + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 745Hz @ 1Hz Intermittent
|
||||
local function gen_tone_4()
|
||||
local t, dt = 0, _2_PI * 745 / _DRATE
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[4].component[1][i] = val
|
||||
TONES[4].component[3][i] = val
|
||||
TONES[4].component[2][i] = 0
|
||||
TONES[4].component[4][i] = 0
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 800Hz @ 0.25s On 1.75s Off
|
||||
local function gen_tone_5()
|
||||
local t, dt = 0, _2_PI * 800 / _DRATE
|
||||
local stop_at = ms_to_samples(250)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
|
||||
if i > stop_at then
|
||||
TONES[5].component[1][i] = val
|
||||
else
|
||||
TONES[5].component[1][i] = 0
|
||||
end
|
||||
|
||||
TONES[5].component[2][i] = 0
|
||||
TONES[5].component[3][i] = 0
|
||||
TONES[5].component[4][i] = 0
|
||||
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 1000/800Hz @ 0.25s Alternating
|
||||
local function gen_tone_6()
|
||||
local t1, dt1 = 0, _2_PI * 1000 / _DRATE
|
||||
local t2, dt2 = 0, _2_PI * 800 / _DRATE
|
||||
|
||||
local alternate_at = ms_to_samples(250)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val
|
||||
if i <= alternate_at then
|
||||
val = math.floor(math.sin(t1) * _MAX_VAL)
|
||||
t1 = (t1 + dt1) % _2_PI
|
||||
else
|
||||
val = math.floor(math.sin(t2) * _MAX_VAL)
|
||||
t2 = (t2 + dt2) % _2_PI
|
||||
end
|
||||
|
||||
TONES[6].component[1][i] = val
|
||||
TONES[6].component[2][i] = val
|
||||
TONES[6].component[3][i] = val
|
||||
TONES[6].component[4][i] = val
|
||||
end
|
||||
end
|
||||
|
||||
-- 1KHz 1s on, 1s off Intermittent
|
||||
local function gen_tone_7()
|
||||
local t, dt = 0, _2_PI * 1000 / _DRATE
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
TONES[7].component[1][i] = val
|
||||
TONES[7].component[2][i] = val
|
||||
TONES[7].component[3][i] = 0
|
||||
TONES[7].component[4][i] = 0
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
end
|
||||
|
||||
-- 1800Hz @ 4Hz Intermittent
|
||||
local function gen_tone_8()
|
||||
local t, dt = 0, _2_PI * 1800 / _DRATE
|
||||
|
||||
local off_at = ms_to_samples(250)
|
||||
|
||||
for i = 1, _05s_SAMPLES do
|
||||
local val = 0
|
||||
|
||||
if i <= off_at then
|
||||
val = math.floor(math.sin(t) * _MAX_VAL)
|
||||
t = (t + dt) % _2_PI
|
||||
end
|
||||
|
||||
TONES[8].component[1][i] = val
|
||||
TONES[8].component[2][i] = val
|
||||
TONES[8].component[3][i] = val
|
||||
TONES[8].component[4][i] = val
|
||||
end
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
-- hard audio limiter
|
||||
---@nodiscard
|
||||
---@param output number output level
|
||||
---@return number limited -128.0 to 127.0
|
||||
local function limit(output)
|
||||
return math.max(-128, math.min(127, output))
|
||||
end
|
||||
|
||||
-- zero the alarm audio buffer
|
||||
local function zero()
|
||||
for i = 1, 4 do
|
||||
for s = 1, _05s_SAMPLES do alarm_ctl.quad_buffer[i][s] = 0 end
|
||||
end
|
||||
end
|
||||
|
||||
-- add an alarm to the output buffer
|
||||
---@param alarm_idx integer tone ID
|
||||
local function add(alarm_idx)
|
||||
alarm_ctl.num_active = alarm_ctl.num_active + 1
|
||||
TONES[alarm_idx].active = true
|
||||
|
||||
for i = 1, 4 do
|
||||
for s = 1, _05s_SAMPLES do
|
||||
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] + TONES[alarm_idx].component[i][s])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- start audio or continue audio on buffer empty
|
||||
---@return boolean success successfully added buffer to audio output
|
||||
local function play()
|
||||
if not alarm_ctl.playing then
|
||||
alarm_ctl.playing = true
|
||||
alarm_ctl.next_block = 1
|
||||
|
||||
return sounder.continue()
|
||||
else
|
||||
return true
|
||||
end
|
||||
else return true end
|
||||
end
|
||||
|
||||
-- initialize the annunciator alarm system
|
||||
@@ -273,23 +29,10 @@ end
|
||||
function sounder.init(speaker, volume)
|
||||
alarm_ctl.speaker = speaker
|
||||
alarm_ctl.speaker.stop()
|
||||
|
||||
alarm_ctl.volume = volume
|
||||
alarm_ctl.playing = false
|
||||
alarm_ctl.num_active = 0
|
||||
alarm_ctl.next_block = 1
|
||||
alarm_ctl.stream.stop()
|
||||
|
||||
zero()
|
||||
|
||||
-- generate tones
|
||||
gen_tone_1()
|
||||
gen_tone_2()
|
||||
gen_tone_3()
|
||||
gen_tone_4()
|
||||
gen_tone_5()
|
||||
gen_tone_6()
|
||||
gen_tone_7()
|
||||
gen_tone_8()
|
||||
audio.generate_tones()
|
||||
end
|
||||
|
||||
-- reconnect the speaker peripheral
|
||||
@@ -297,173 +40,40 @@ end
|
||||
function sounder.reconnect(speaker)
|
||||
alarm_ctl.speaker = speaker
|
||||
alarm_ctl.playing = false
|
||||
alarm_ctl.next_block = 1
|
||||
alarm_ctl.num_active = 0
|
||||
for id = 1, #TONES do TONES[id].active = false end
|
||||
alarm_ctl.stream.stop()
|
||||
end
|
||||
|
||||
-- check alarm state to enable/disable alarms
|
||||
---@param units table|nil unit list or nil to use test mode
|
||||
function sounder.eval(units)
|
||||
local changed = false
|
||||
local any_active = false
|
||||
local new_states = { false, false, false, false, false, false, false, false }
|
||||
local alarms = { false, false, false, false, false, false, false, false, false, false, false, false }
|
||||
-- set alarm tones
|
||||
---@param states table alarm tone commands from supervisor
|
||||
function sounder.set(states)
|
||||
-- set tone states
|
||||
for id = 1, #states do alarm_ctl.stream.set_active(id, states[id]) end
|
||||
|
||||
if units ~= nil then
|
||||
-- check all alarms for all units
|
||||
for i = 1, #units do
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
for id = 1, #unit.alarms do
|
||||
alarms[id] = alarms[id] or (unit.alarms[id] == ALARM_STATE.TRIPPED)
|
||||
end
|
||||
end
|
||||
else
|
||||
alarms = test_alarms
|
||||
end
|
||||
|
||||
-- containment breach is worst case CRITICAL alarm, this takes priority
|
||||
if alarms[ALARM.ContainmentBreach] then
|
||||
new_states[T_1800Hz_Int_4Hz] = true
|
||||
else
|
||||
-- critical damage is highest priority CRITICAL level alarm
|
||||
if alarms[ALARM.CriticalDamage] then
|
||||
new_states[T_660Hz_Int_125ms] = true
|
||||
else
|
||||
-- EMERGENCY level alarms + URGENT over temp
|
||||
if alarms[ALARM.ReactorDamage] or alarms[ALARM.ReactorOverTemp] or alarms[ALARM.ReactorWasteLeak] then
|
||||
new_states[T_544Hz_440Hz_Alt] = true
|
||||
-- URGENT level turbine trip
|
||||
elseif alarms[ALARM.TurbineTrip] then
|
||||
new_states[T_745Hz_Int_1Hz] = true
|
||||
-- URGENT level reactor lost
|
||||
elseif alarms[ALARM.ReactorLost] then
|
||||
new_states[T_340Hz_Int_2Hz] = true
|
||||
-- TIMELY level alarms
|
||||
elseif alarms[ALARM.ReactorHighTemp] or alarms[ALARM.ReactorHighWaste] or alarms[ALARM.RCSTransient] then
|
||||
new_states[T_800Hz_Int] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- check RPS transient URGENT level alarm
|
||||
if alarms[ALARM.RPSTransient] then
|
||||
new_states[T_1000Hz_Int] = true
|
||||
-- disable really painful audio combination
|
||||
new_states[T_340Hz_Int_2Hz] = false
|
||||
end
|
||||
end
|
||||
|
||||
-- radiation is a big concern, always play this CRITICAL level alarm if active
|
||||
if alarms[ALARM.ContainmentRadiation] then
|
||||
new_states[T_800Hz_1000Hz_Alt] = true
|
||||
-- we are going to disable the RPS trip alarm audio due to conflict, and if it was enabled
|
||||
-- then we can re-enable the reactor lost alarm audio since it doesn't painfully combine with this one
|
||||
if new_states[T_1000Hz_Int] and alarms[ALARM.ReactorLost] then new_states[T_340Hz_Int_2Hz] = true end
|
||||
-- it sounds *really* bad if this is in conjunction with these other tones, so disable them
|
||||
new_states[T_745Hz_Int_1Hz] = false
|
||||
new_states[T_800Hz_Int] = false
|
||||
new_states[T_1000Hz_Int] = false
|
||||
end
|
||||
|
||||
-- check if any changed, check if any active, update active flags
|
||||
for id = 1, #TONES do
|
||||
if new_states[id] ~= TONES[id].active then
|
||||
TONES[id].active = new_states[id]
|
||||
changed = true
|
||||
end
|
||||
|
||||
if TONES[id].active then any_active = true end
|
||||
end
|
||||
|
||||
-- zero and re-add tones if changed
|
||||
if changed then
|
||||
zero()
|
||||
|
||||
for id = 1, #TONES do
|
||||
if TONES[id].active then add(id) end
|
||||
end
|
||||
end
|
||||
|
||||
if any_active then play() else sounder.stop() end
|
||||
-- re-compute output if needed, then play audio if available
|
||||
if alarm_ctl.stream.is_recompute_needed() then alarm_ctl.stream.compute_buffer() end
|
||||
if alarm_ctl.stream.any_active() then play() else sounder.stop() end
|
||||
end
|
||||
|
||||
-- stop all audio and clear output buffer
|
||||
function sounder.stop()
|
||||
alarm_ctl.playing = false
|
||||
alarm_ctl.speaker.stop()
|
||||
alarm_ctl.next_block = 1
|
||||
alarm_ctl.num_active = 0
|
||||
for id = 1, #TONES do TONES[id].active = false end
|
||||
zero()
|
||||
alarm_ctl.stream.stop()
|
||||
end
|
||||
|
||||
-- continue audio on buffer empty
|
||||
---@return boolean success successfully added buffer to audio output
|
||||
function sounder.continue()
|
||||
local success = false
|
||||
|
||||
if alarm_ctl.playing then
|
||||
if alarm_ctl.speaker ~= nil and #alarm_ctl.quad_buffer[alarm_ctl.next_block] > 0 then
|
||||
local success = alarm_ctl.speaker.playAudio(alarm_ctl.quad_buffer[alarm_ctl.next_block], alarm_ctl.volume)
|
||||
|
||||
alarm_ctl.next_block = alarm_ctl.next_block + 1
|
||||
if alarm_ctl.next_block > 4 then alarm_ctl.next_block = 1 end
|
||||
|
||||
if not success then
|
||||
log.debug("SOUNDER: error playing audio")
|
||||
end
|
||||
|
||||
return success
|
||||
else
|
||||
return false
|
||||
end
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
--#region Test Functions
|
||||
|
||||
function sounder.test_1() add(1) play() end -- play tone T_340Hz_Int_2Hz
|
||||
function sounder.test_2() add(2) play() end -- play tone T_544Hz_440Hz_Alt
|
||||
function sounder.test_3() add(3) play() end -- play tone T_660Hz_Int_125ms
|
||||
function sounder.test_4() add(4) play() end -- play tone T_745Hz_Int_1Hz
|
||||
function sounder.test_5() add(5) play() end -- play tone T_800Hz_Int
|
||||
function sounder.test_6() add(6) play() end -- play tone T_800Hz_1000Hz_Alt
|
||||
function sounder.test_7() add(7) play() end -- play tone T_1000Hz_Int
|
||||
function sounder.test_8() add(8) play() end -- play tone T_1800Hz_Int_4Hz
|
||||
|
||||
function sounder.test_breach(active) test_alarms[ALARM.ContainmentBreach] = active end ---@param active boolean
|
||||
function sounder.test_rad(active) test_alarms[ALARM.ContainmentRadiation] = active end ---@param active boolean
|
||||
function sounder.test_lost(active) test_alarms[ALARM.ReactorLost] = active end ---@param active boolean
|
||||
function sounder.test_crit(active) test_alarms[ALARM.CriticalDamage] = active end ---@param active boolean
|
||||
function sounder.test_dmg(active) test_alarms[ALARM.ReactorDamage] = active end ---@param active boolean
|
||||
function sounder.test_overtemp(active) test_alarms[ALARM.ReactorOverTemp] = active end ---@param active boolean
|
||||
function sounder.test_hightemp(active) test_alarms[ALARM.ReactorHighTemp] = active end ---@param active boolean
|
||||
function sounder.test_wasteleak(active) test_alarms[ALARM.ReactorWasteLeak] = active end ---@param active boolean
|
||||
function sounder.test_highwaste(active) test_alarms[ALARM.ReactorHighWaste] = active end ---@param active boolean
|
||||
function sounder.test_rps(active) test_alarms[ALARM.RPSTransient] = active end ---@param active boolean
|
||||
function sounder.test_rcs(active) test_alarms[ALARM.RCSTransient] = active end ---@param active boolean
|
||||
function sounder.test_turbinet(active) test_alarms[ALARM.TurbineTrip] = active end ---@param active boolean
|
||||
|
||||
-- power rescaling limiter test
|
||||
function sounder.test_power_scale()
|
||||
local start = util.time_ms()
|
||||
|
||||
zero()
|
||||
|
||||
for id = 1, #TONES do
|
||||
if TONES[id].active then
|
||||
for i = 1, 4 do
|
||||
for s = 1, _05s_SAMPLES do
|
||||
alarm_ctl.quad_buffer[i][s] = limit(alarm_ctl.quad_buffer[i][s] +
|
||||
(TONES[id].component[i][s] / math.sqrt(alarm_ctl.num_active)))
|
||||
end
|
||||
end
|
||||
if alarm_ctl.speaker ~= nil and alarm_ctl.stream.has_next_block() then
|
||||
success = alarm_ctl.speaker.playAudio(alarm_ctl.stream.get_next_block(), alarm_ctl.volume)
|
||||
if not success then log.error("SOUNDER: error playing audio") end
|
||||
end
|
||||
end
|
||||
|
||||
log.debug("SOUNDER: power rescale test took " .. (util.time_ms() - start) .. "ms")
|
||||
return success
|
||||
end
|
||||
|
||||
--#endregion
|
||||
|
||||
return sounder
|
||||
|
||||
@@ -4,23 +4,25 @@
|
||||
|
||||
require("/initenv").init_env()
|
||||
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcallbackdsp = require("scada-common.tcallbackdsp")
|
||||
local util = require("scada-common.util")
|
||||
local comms = require("scada-common.comms")
|
||||
local crash = require("scada-common.crash")
|
||||
local log = require("scada-common.log")
|
||||
local network = require("scada-common.network")
|
||||
local ppm = require("scada-common.ppm")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local core = require("graphics.core")
|
||||
|
||||
local config = require("coordinator.config")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
local configure = require("coordinator.configure")
|
||||
local coordinator = require("coordinator.coordinator")
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
local renderer = require("coordinator.renderer")
|
||||
local sounder = require("coordinator.sounder")
|
||||
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
local apisessions = require("coordinator.session.apisessions")
|
||||
|
||||
local COORDINATOR_VERSION = "v0.15.2"
|
||||
local COORDINATOR_VERSION = "v1.2.2"
|
||||
|
||||
local println = util.println
|
||||
local println_ts = util.println_ts
|
||||
@@ -29,35 +31,37 @@ local log_graphics = coordinator.log_graphics
|
||||
local log_sys = coordinator.log_sys
|
||||
local log_boot = coordinator.log_boot
|
||||
local log_comms = coordinator.log_comms
|
||||
local log_comms_connecting = coordinator.log_comms_connecting
|
||||
local log_crypto = coordinator.log_crypto
|
||||
|
||||
----------------------------------------
|
||||
-- config validation
|
||||
-- get configuration
|
||||
----------------------------------------
|
||||
|
||||
local cfv = util.new_validator()
|
||||
-- mount connected devices (required for monitor setup)
|
||||
ppm.mount_all()
|
||||
|
||||
cfv.assert_port(config.SCADA_SV_PORT)
|
||||
cfv.assert_port(config.SCADA_SV_CTL_LISTEN)
|
||||
cfv.assert_port(config.SCADA_API_LISTEN)
|
||||
cfv.assert_type_int(config.TRUSTED_RANGE)
|
||||
cfv.assert_type_num(config.SV_TIMEOUT)
|
||||
cfv.assert_min(config.SV_TIMEOUT, 2)
|
||||
cfv.assert_type_num(config.API_TIMEOUT)
|
||||
cfv.assert_min(config.API_TIMEOUT, 2)
|
||||
cfv.assert_type_int(config.NUM_UNITS)
|
||||
cfv.assert_type_num(config.SOUNDER_VOLUME)
|
||||
cfv.assert_type_bool(config.TIME_24_HOUR)
|
||||
cfv.assert_type_str(config.LOG_PATH)
|
||||
cfv.assert_type_int(config.LOG_MODE)
|
||||
local loaded, monitors = coordinator.load_config()
|
||||
if loaded ~= 0 then
|
||||
-- try to reconfigure (user action)
|
||||
local success, error = configure.configure(loaded, monitors)
|
||||
if success then
|
||||
loaded, monitors = coordinator.load_config()
|
||||
assert(loaded == 0, util.trinary(loaded == 1, "failed to load valid configuration", "monitor configuration invalid"))
|
||||
else
|
||||
assert(success, "coordinator configuration error: " .. error)
|
||||
end
|
||||
end
|
||||
|
||||
assert(cfv.valid(), "bad config file: missing/invalid fields")
|
||||
-- passed checks, good now
|
||||
---@cast monitors monitors_struct
|
||||
|
||||
local config = coordinator.config
|
||||
|
||||
----------------------------------------
|
||||
-- log init
|
||||
----------------------------------------
|
||||
|
||||
log.init(config.LOG_PATH, config.LOG_MODE, config.LOG_DEBUG == true)
|
||||
log.init(config.LogPath, config.LogMode, config.LogDebug)
|
||||
|
||||
log.info("========================================")
|
||||
log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION)
|
||||
@@ -75,31 +79,16 @@ local function main()
|
||||
-- system startup
|
||||
----------------------------------------
|
||||
|
||||
-- mount connected devices
|
||||
-- re-mount devices now that logging is ready
|
||||
ppm.mount_all()
|
||||
|
||||
-- setup monitors
|
||||
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
|
||||
if not configured or monitors == nil then
|
||||
println("startup> monitor setup failed")
|
||||
log.fatal("monitor configuration failed")
|
||||
return
|
||||
end
|
||||
-- report versions/init fp PSIL
|
||||
iocontrol.init_fp(COORDINATOR_VERSION, comms.version)
|
||||
|
||||
-- init renderer
|
||||
renderer.legacy_disable_flow_view(config.DisableFlowView)
|
||||
renderer.set_displays(monitors)
|
||||
renderer.init_displays()
|
||||
|
||||
if not renderer.validate_main_display_width() then
|
||||
println("startup> main display must be 8 blocks wide")
|
||||
log.fatal("main display not wide enough")
|
||||
return
|
||||
elseif not renderer.validate_unit_display_sizes() then
|
||||
println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks")
|
||||
log.fatal("unit display dimensions incorrect")
|
||||
return
|
||||
end
|
||||
|
||||
renderer.init_dmesg()
|
||||
|
||||
-- lets get started!
|
||||
@@ -122,15 +111,22 @@ local function main()
|
||||
else
|
||||
local sounder_start = util.time_ms()
|
||||
log_boot("annunciator alarm speaker connected")
|
||||
sounder.init(speaker, config.SOUNDER_VOLUME)
|
||||
sounder.init(speaker, config.SpeakerVolume)
|
||||
log_boot("tone generation took " .. (util.time_ms() - sounder_start) .. "ms")
|
||||
log_sys("annunciator alarm configured")
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
|
||||
----------------------------------------
|
||||
-- setup communications
|
||||
----------------------------------------
|
||||
|
||||
-- message authentication init
|
||||
if type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0 then
|
||||
local init_time = network.init_mac(config.AuthKey)
|
||||
log_crypto("HMAC init took " .. init_time .. "ms")
|
||||
end
|
||||
|
||||
-- get the communications modem
|
||||
local modem = ppm.get_wireless_modem()
|
||||
if modem == nil then
|
||||
@@ -140,16 +136,17 @@ local function main()
|
||||
return
|
||||
else
|
||||
log_comms("wireless modem connected")
|
||||
iocontrol.fp_has_modem(true)
|
||||
end
|
||||
|
||||
-- create connection watchdog
|
||||
local conn_watchdog = util.new_watchdog(config.SV_TIMEOUT)
|
||||
local conn_watchdog = util.new_watchdog(config.SVR_Timeout)
|
||||
conn_watchdog.cancel()
|
||||
log.debug("startup> conn watchdog created")
|
||||
|
||||
-- start comms, open all channels
|
||||
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_CTL_LISTEN,
|
||||
config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog)
|
||||
-- create network interface then setup comms
|
||||
local nic = network.nic(modem)
|
||||
local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, conn_watchdog)
|
||||
log.debug("startup> comms init")
|
||||
log_comms("comms initialized")
|
||||
|
||||
@@ -158,78 +155,52 @@ local function main()
|
||||
local loop_clock = util.new_clock(MAIN_CLOCK)
|
||||
|
||||
----------------------------------------
|
||||
-- connect to the supervisor
|
||||
-- start front panel & UI start function
|
||||
----------------------------------------
|
||||
|
||||
-- attempt to connect to the supervisor or exit
|
||||
local function init_connect_sv()
|
||||
local tick_waiting, task_done = log_comms_connecting("attempting to connect to configured supervisor on channel " .. config.SCADA_SV_PORT)
|
||||
log_graphics("starting front panel UI...")
|
||||
|
||||
-- attempt to establish a connection with the supervisory computer
|
||||
if not coord_comms.sv_connect(60, tick_waiting, task_done) then
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
if not init_connect_sv() then
|
||||
println("startup> failed to connect to supervisor")
|
||||
log_sys("system shutdown")
|
||||
local fp_ok, fp_message = renderer.try_start_fp()
|
||||
if not fp_ok then
|
||||
log_graphics(util.c("front panel UI error: ", fp_message))
|
||||
println_ts("front panel UI creation failed")
|
||||
log.fatal(util.c("front panel GUI render failed with error ", fp_message))
|
||||
return
|
||||
else
|
||||
log_sys("supervisor connected, proceeding to UI start")
|
||||
end
|
||||
else log_graphics("front panel ready") end
|
||||
|
||||
----------------------------------------
|
||||
-- start the UI
|
||||
----------------------------------------
|
||||
|
||||
-- start up the UI
|
||||
-- start up the main UI
|
||||
---@return boolean ui_ok started ok
|
||||
local function init_start_ui()
|
||||
log_graphics("starting UI...")
|
||||
local function start_main_ui()
|
||||
log_graphics("starting main UI...")
|
||||
|
||||
local draw_start = util.time_ms()
|
||||
|
||||
local ui_ok, message = pcall(renderer.start_ui)
|
||||
local ui_ok, ui_message = renderer.try_start_ui()
|
||||
if not ui_ok then
|
||||
renderer.close_ui()
|
||||
log_graphics(util.c("UI crashed: ", message))
|
||||
println_ts("UI crashed")
|
||||
log.fatal(util.c("GUI crashed with error ", message))
|
||||
log_graphics(util.c("main UI error: ", ui_message))
|
||||
log.fatal(util.c("main GUI render failed with error ", ui_message))
|
||||
else
|
||||
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
log_graphics("main UI draw took " .. (util.time_ms() - draw_start) .. "ms")
|
||||
end
|
||||
|
||||
return ui_ok
|
||||
end
|
||||
|
||||
local ui_ok = init_start_ui()
|
||||
|
||||
----------------------------------------
|
||||
-- main event loop
|
||||
----------------------------------------
|
||||
|
||||
local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
|
||||
local link_failed = false
|
||||
local ui_ok = true
|
||||
local date_format = util.trinary(config.Time24Hour, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y")
|
||||
|
||||
local no_modem = false
|
||||
-- start clock
|
||||
loop_clock.start()
|
||||
|
||||
if ui_ok then
|
||||
-- start connection watchdog
|
||||
conn_watchdog.feed()
|
||||
log.debug("startup> conn watchdog started")
|
||||
|
||||
log_sys("system started successfully")
|
||||
end
|
||||
log_sys("system started successfully")
|
||||
|
||||
-- main event loop
|
||||
while ui_ok do
|
||||
while true do
|
||||
local event, param1, param2, param3, param4, param5 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
@@ -239,33 +210,36 @@ local function main()
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
-- we only really care if this is our wireless modem
|
||||
if device == modem then
|
||||
no_modem = true
|
||||
-- if it is another modem, handle other peripheral losses separately
|
||||
if nic.is_modem(device) then
|
||||
nic.disconnect()
|
||||
log_sys("comms modem disconnected")
|
||||
println_ts("wireless modem disconnected!")
|
||||
|
||||
-- close out UI
|
||||
renderer.close_ui()
|
||||
local other_modem = ppm.get_wireless_modem()
|
||||
if other_modem then
|
||||
log_sys("found another wireless modem, using it for comms")
|
||||
nic.connect(other_modem)
|
||||
else
|
||||
-- close out main UI
|
||||
renderer.close_ui()
|
||||
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
-- alert user to status
|
||||
log_sys("awaiting comms modem reconnect...")
|
||||
|
||||
iocontrol.fp_has_modem(false)
|
||||
end
|
||||
else
|
||||
log_sys("non-comms modem disconnected")
|
||||
end
|
||||
elseif type == "monitor" then
|
||||
if renderer.is_monitor_used(device) then
|
||||
-- "halt and catch fire" style handling
|
||||
local msg = "lost a configured monitor, system will now exit"
|
||||
println_ts(msg)
|
||||
log_sys(msg)
|
||||
break
|
||||
if renderer.handle_disconnect(device) then
|
||||
log_sys("lost a configured monitor")
|
||||
else
|
||||
log_sys("lost unused monitor, ignoring")
|
||||
log_sys("lost an unused monitor")
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
local msg = "lost alarm sounder speaker"
|
||||
println_ts(msg)
|
||||
log_sys(msg)
|
||||
log_sys("lost alarm sounder speaker")
|
||||
iocontrol.fp_has_speaker(false)
|
||||
end
|
||||
end
|
||||
elseif event == "peripheral" then
|
||||
@@ -273,34 +247,50 @@ local function main()
|
||||
|
||||
if type ~= nil and device ~= nil then
|
||||
if type == "modem" then
|
||||
if device.isWireless() then
|
||||
if device.isWireless() and not nic.is_connected() then
|
||||
-- reconnected modem
|
||||
no_modem = false
|
||||
modem = device
|
||||
coord_comms.reconnect_modem(modem)
|
||||
|
||||
log_sys("comms modem reconnected")
|
||||
println_ts("wireless modem reconnected.")
|
||||
|
||||
-- re-init system
|
||||
if not init_connect_sv() then break end
|
||||
ui_ok = init_start_ui()
|
||||
nic.connect(device)
|
||||
iocontrol.fp_has_modem(true)
|
||||
elseif device.isWireless() then
|
||||
log.info("unused wireless modem reconnected")
|
||||
else
|
||||
log_sys("wired modem reconnected")
|
||||
end
|
||||
-- elseif type == "monitor" then
|
||||
-- not supported, system will exit on loss of in-use monitors
|
||||
elseif type == "monitor" then
|
||||
if renderer.handle_reconnect(param1, device) then
|
||||
log_sys(util.c("configured monitor ", param1, " reconnected"))
|
||||
else
|
||||
log_sys(util.c("unused monitor ", param1, " connected"))
|
||||
end
|
||||
elseif type == "speaker" then
|
||||
local msg = "alarm sounder speaker reconnected"
|
||||
println_ts(msg)
|
||||
log_sys(msg)
|
||||
log_sys("alarm sounder speaker reconnected")
|
||||
sounder.reconnect(device)
|
||||
iocontrol.fp_has_speaker(true)
|
||||
end
|
||||
end
|
||||
elseif event == "timer" then
|
||||
if loop_clock.is_clock(param1) then
|
||||
-- main loop tick
|
||||
|
||||
-- toggle heartbeat
|
||||
iocontrol.heartbeat()
|
||||
|
||||
-- maintain connection
|
||||
if nic.is_connected() then
|
||||
local ok, start_ui = coord_comms.try_connect()
|
||||
if not ok then
|
||||
link_failed = true
|
||||
log_sys("supervisor connection failed, shutting down...")
|
||||
log.fatal("failed to connect to supervisor")
|
||||
break
|
||||
elseif start_ui then
|
||||
log_sys("supervisor connected, proceeding to main UI start")
|
||||
ui_ok = start_main_ui()
|
||||
if not ui_ok then break end
|
||||
end
|
||||
end
|
||||
|
||||
-- iterate sessions
|
||||
apisessions.iterate_all()
|
||||
|
||||
@@ -308,25 +298,19 @@ local function main()
|
||||
apisessions.free_all_closed()
|
||||
|
||||
-- update date and time string for main display
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
|
||||
if coord_comms.is_linked() then
|
||||
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
|
||||
end
|
||||
|
||||
loop_clock.start()
|
||||
elseif conn_watchdog.is_timer(param1) then
|
||||
-- supervisor watchdog timeout
|
||||
local msg = "supervisor server timeout"
|
||||
log_comms(msg)
|
||||
println_ts(msg)
|
||||
log_comms("supervisor server timeout")
|
||||
|
||||
-- close connection, UI, and stop sounder
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
|
||||
if not no_modem then
|
||||
-- try to re-connect to the supervisor
|
||||
if not init_connect_sv() then break end
|
||||
ui_ok = init_start_ui()
|
||||
end
|
||||
else
|
||||
-- a non-clock/main watchdog timer event
|
||||
|
||||
@@ -334,30 +318,24 @@ local function main()
|
||||
apisessions.check_all_watchdogs(param1)
|
||||
|
||||
-- notify timer callback dispatcher
|
||||
tcallbackdsp.handle(param1)
|
||||
tcd.handle(param1)
|
||||
end
|
||||
elseif event == "modem_message" then
|
||||
-- got a packet
|
||||
local packet = coord_comms.parse_packet(param1, param2, param3, param4, param5)
|
||||
coord_comms.handle_packet(packet)
|
||||
|
||||
-- check if it was a disconnect
|
||||
if not coord_comms.is_linked() then
|
||||
-- handle then check if it was a disconnect
|
||||
if coord_comms.handle_packet(packet) then
|
||||
log_comms("supervisor closed connection")
|
||||
|
||||
-- close connection, UI, and stop sounder
|
||||
-- close connection, main UI, and stop sounder
|
||||
coord_comms.close()
|
||||
renderer.close_ui()
|
||||
sounder.stop()
|
||||
|
||||
if not no_modem then
|
||||
-- try to re-connect to the supervisor
|
||||
if not init_connect_sv() then break end
|
||||
ui_ok = init_start_ui()
|
||||
end
|
||||
end
|
||||
elseif event == "monitor_touch" then
|
||||
-- handle a monitor touch event
|
||||
elseif event == "monitor_touch" or event == "mouse_click" or event == "mouse_up" or
|
||||
event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
-- handle a mouse event
|
||||
renderer.handle_mouse(core.events.new_mouse_event(event, param1, param2, param3))
|
||||
elseif event == "speaker_audio_empty" then
|
||||
-- handle speaker buffer emptied
|
||||
@@ -366,10 +344,17 @@ local function main()
|
||||
|
||||
-- check for termination request
|
||||
if event == "terminate" or ppm.should_terminate() then
|
||||
println_ts("terminate requested, closing connections...")
|
||||
log_comms("terminate requested, closing supervisor connection...")
|
||||
-- handle supervisor connection
|
||||
coord_comms.try_connect(true)
|
||||
|
||||
if coord_comms.is_linked() then
|
||||
log_comms("terminate requested, closing supervisor connection...")
|
||||
else link_failed = true end
|
||||
|
||||
coord_comms.close()
|
||||
log_comms("supervisor connection closed")
|
||||
|
||||
-- handle API sessions
|
||||
log_comms("closing api sessions...")
|
||||
apisessions.close_all()
|
||||
log_comms("api sessions closed")
|
||||
@@ -378,15 +363,23 @@ local function main()
|
||||
end
|
||||
|
||||
renderer.close_ui()
|
||||
renderer.close_fp()
|
||||
sounder.stop()
|
||||
log_sys("system shutdown")
|
||||
|
||||
if link_failed then println_ts("failed to connect to supervisor") end
|
||||
if not ui_ok then println_ts("main UI creation failed") end
|
||||
|
||||
-- close on error exit (such as UI error)
|
||||
if coord_comms.is_linked() then coord_comms.close() end
|
||||
|
||||
println_ts("exited")
|
||||
log.info("exited")
|
||||
end
|
||||
|
||||
if not xpcall(main, crash.handler) then
|
||||
pcall(renderer.close_ui)
|
||||
pcall(renderer.close_fp)
|
||||
pcall(sounder.stop)
|
||||
crash.exit()
|
||||
else
|
||||
|
||||
@@ -12,20 +12,19 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
|
||||
-- new boiler view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local boiler = Rectangle{parent=root,border=border(1, colors.gray, true),width=31,height=7,x=x,y=y}
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
local boiler = Rectangle{parent=root,border=border(1,colors.gray,true),width=31,height=7,x=x,y=y}
|
||||
|
||||
local status = StateIndicator{parent=boiler,x=9,y=1,states=style.boiler.states,value=1,min_width=12}
|
||||
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
|
||||
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
|
||||
local temp = DataIndicator{parent=boiler,x=5,y=3,lu_colors=style.lu_col,label="Temp:",unit="K",format="%10.2f",value=0,width=22,fg_bg=text_fg_bg}
|
||||
local boil_r = DataIndicator{parent=boiler,x=5,y=4,lu_colors=style.lu_col,label="Boil:",unit="mB/t",format="%10.0f",value=0,commas=true,width=22,fg_bg=text_fg_bg}
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
temp.register(ps, "temperature", temp.update)
|
||||
|
||||
@@ -16,7 +16,10 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- new induction matrix view
|
||||
---@param root graphics_element parent
|
||||
@@ -31,14 +34,12 @@ local function new_view(root, x, y, data, ps, id)
|
||||
|
||||
local matrix = Div{parent=root,fg_bg=style.root,width=33,height=24,x=x,y=y}
|
||||
|
||||
TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=cpair(colors.lightGray,colors.gray)}
|
||||
TextBox{parent=matrix,text=title,alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.gray)}
|
||||
TextBox{parent=matrix,text=" ",width=33,height=1,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=matrix,text=title,alignment=ALIGN.CENTER,width=33,height=1,x=1,y=2,fg_bg=style.lg_gray}
|
||||
|
||||
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local label_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
|
||||
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
|
||||
local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
@@ -83,9 +84,7 @@ local function new_view(root, x, y, data, ps, id)
|
||||
local function calc_saturation(val)
|
||||
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
|
||||
return val / data.build.transfer_cap
|
||||
else
|
||||
return 0
|
||||
end
|
||||
else return 0 end
|
||||
end
|
||||
|
||||
charge.register(ps, "energy_fill", charge.update)
|
||||
|
||||
53
coordinator/ui/components/pkt_entry.lua
Normal file
53
coordinator/ui/components/pkt_entry.lua
Normal file
@@ -0,0 +1,53 @@
|
||||
--
|
||||
-- Pocket Connection Entry
|
||||
--
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lg_wh = style.lg_white
|
||||
|
||||
-- create a pocket list entry
|
||||
---@param parent graphics_element parent
|
||||
---@param id integer PKT session ID
|
||||
local function init(parent, id)
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
-- root div
|
||||
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2,hidden=true}
|
||||
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.bw_fg_bg}
|
||||
|
||||
local ps_prefix = "pkt_" .. id .. "_"
|
||||
|
||||
TextBox{parent=entry,x=1,y=1,text="",width=8,height=1,fg_bg=text_fg_bg}
|
||||
local pkt_addr = TextBox{parent=entry,x=1,y=2,text="@ C ??",alignment=ALIGN.CENTER,width=8,height=1,fg_bg=text_fg_bg,nav_active=cpair(colors.gray,colors.black)}
|
||||
TextBox{parent=entry,x=1,y=3,text="",width=8,height=1,fg_bg=text_fg_bg}
|
||||
pkt_addr.register(ps, ps_prefix .. "addr", pkt_addr.set_value)
|
||||
|
||||
TextBox{parent=entry,x=10,y=2,text="FW:",width=3,height=1}
|
||||
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,height=1,fg_bg=lg_wh}
|
||||
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
|
||||
|
||||
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4,height=1}
|
||||
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=lg_wh}
|
||||
TextBox{parent=entry,x=46,y=2,text="ms",width=4,height=1,fg_bg=lg_wh}
|
||||
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
|
||||
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return init
|
||||
@@ -1,4 +1,4 @@
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
@@ -15,17 +15,31 @@ local TextBox = require("graphics.elements.textbox")
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local RadIndicator = require("graphics.elements.indicators.rad")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
|
||||
|
||||
local Checkbox = require("graphics.elements.controls.checkbox")
|
||||
local HazardButton = require("graphics.elements.controls.hazard_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local lu_cpair = style.lu_colors
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local dis_colors = style.dis_colors
|
||||
|
||||
local gry_wht = style.gray_white
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
-- new process control view
|
||||
@@ -33,17 +47,16 @@ local period = core.flasher.PERIOD
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
local function new_view(root, x, y)
|
||||
assert(root.height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
assert(root.get_height() >= (y + 24), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
local black = cpair(colors.black, colors.black)
|
||||
local blk_brn = cpair(colors.black, colors.brown)
|
||||
local blk_pur = cpair(colors.black, colors.purple)
|
||||
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local hzd_fg_bg = cpair(colors.white, colors.gray)
|
||||
local lu_cpair = cpair(colors.gray, colors.gray)
|
||||
local dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
local main = Div{parent=root,width=104,height=24,x=x,y=y}
|
||||
local main = Div{parent=root,width=128,height=24,x=x,y=y}
|
||||
|
||||
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
|
||||
local ack_a = HazardButton{parent=main,x=16,y=1,text="ACK \x13",accent=colors.orange,dis_colors=dis_colors,callback=process.fac_ack_alarms,fg_bg=hzd_fg_bg}
|
||||
@@ -51,20 +64,22 @@ local function new_view(root, x, y)
|
||||
facility.scram_ack = scram.on_response
|
||||
facility.ack_alarms_ack = ack_a.on_response
|
||||
|
||||
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=cpair(colors.green,colors.red)}
|
||||
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=cpair(colors.green,colors.gray)}
|
||||
local all_ok = IndicatorLight{parent=main,y=5,label="Unit Systems Online",colors=ind_grn}
|
||||
local rad_mon = TriIndicatorLight{parent=main,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
local ind_mat = IndicatorLight{parent=main,label="Induction Matrix",colors=ind_grn}
|
||||
local sps = IndicatorLight{parent=main,label="SPS Connected",colors=ind_grn}
|
||||
|
||||
all_ok.register(facility.ps, "all_sys_ok", all_ok.update)
|
||||
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
|
||||
rad_mon.register(facility.ps, "rad_computed_status", rad_mon.update)
|
||||
ind_mat.register(facility.induction_ps_tbl[1], "computed_status", function (status) ind_mat.update(status > 1) end)
|
||||
sps.register(facility.sps_ps_tbl[1], "computed_status", function (status) sps.update(status > 1) end)
|
||||
|
||||
main.line_break()
|
||||
|
||||
local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=cpair(colors.green,colors.red)}
|
||||
local auto_act = IndicatorLight{parent=main,label="Process Active",colors=cpair(colors.green,colors.gray)}
|
||||
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=cpair(colors.yellow,colors.gray)}
|
||||
local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=ind_grn}
|
||||
local auto_act = IndicatorLight{parent=main,label="Process Active",colors=ind_grn}
|
||||
local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=ind_wht,flash=true,period=period.BLINK_250_MS}
|
||||
local auto_sat = IndicatorLight{parent=main,label="Min/Max Burn Rate",colors=ind_yel}
|
||||
|
||||
auto_ready.register(facility.ps, "auto_ready", auto_ready.update)
|
||||
auto_act.register(facility.ps, "auto_active", auto_act.update)
|
||||
@@ -73,12 +88,12 @@ local function new_view(root, x, y)
|
||||
|
||||
main.line_break()
|
||||
|
||||
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local unit_crit = IndicatorLight{parent=main,label="Unit Critical Alarm",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local matrix_dc = IndicatorLight{parent=main,label="Matrix Disconnected",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
local matrix_fill = IndicatorLight{parent=main,label="Matrix Charge High",colors=ind_red,flash=true,period=period.BLINK_500_MS}
|
||||
local unit_crit = IndicatorLight{parent=main,label="Unit Critical Alarm",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local fac_rad_h = IndicatorLight{parent=main,label="Facility Radiation High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local gen_fault = IndicatorLight{parent=main,label="Gen. Control Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
|
||||
auto_scram.register(facility.ps, "auto_scram", auto_scram.update)
|
||||
matrix_dc.register(facility.ps, "as_matrix_dc", matrix_dc.update)
|
||||
@@ -99,7 +114,7 @@ local function new_view(root, x, y)
|
||||
-- process control --
|
||||
---------------------
|
||||
|
||||
local proc = Div{parent=main,width=78,height=24,x=27,y=1}
|
||||
local proc = Div{parent=main,width=103,height=24,x=27,y=1}
|
||||
|
||||
-----------------------------
|
||||
-- process control targets --
|
||||
@@ -107,35 +122,35 @@ local function new_view(root, x, y)
|
||||
|
||||
local targets = Div{parent=proc,width=31,height=24,x=1,y=1}
|
||||
|
||||
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
||||
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
|
||||
|
||||
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=gry_wht}
|
||||
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
|
||||
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
b_target.register(facility.ps, "process_burn_target", b_target.set_value)
|
||||
burn_sum.register(facility.ps, "burn_sum", burn_sum.update)
|
||||
|
||||
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
||||
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
|
||||
|
||||
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=gry_wht}
|
||||
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=chg_target,x=18,y=2,text="MFE"}
|
||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
c_target.register(facility.ps, "process_charge_target", c_target.set_value)
|
||||
cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end)
|
||||
|
||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
|
||||
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur}
|
||||
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
|
||||
|
||||
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=gry_wht}
|
||||
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
|
||||
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
|
||||
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=black,width=23,fg_bg=blk_brn}
|
||||
|
||||
g_target.register(facility.ps, "process_gen_target", g_target.set_value)
|
||||
cur_gen.register(facility.induction_ps_tbl[1], "last_input", function (j) cur_gen.update(util.round(util.joules_to_fe(j) / 1000)) end)
|
||||
@@ -148,46 +163,77 @@ local function new_view(root, x, y)
|
||||
|
||||
local rate_limits = {}
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
for i = 1, 4 do
|
||||
local unit
|
||||
local tag_fg_bg = gry_wht
|
||||
local lim_fg_bg = style.lg_white
|
||||
local ctl_fg = colors.lightGray
|
||||
local cur_fg_bg = style.lg_white
|
||||
local cur_lu = colors.lightGray
|
||||
|
||||
if i <= facility.num_units then
|
||||
unit = units[i] ---@type ioctl_unit
|
||||
tag_fg_bg = cpair(colors.black,colors.lightBlue)
|
||||
lim_fg_bg = bw_fg_bg
|
||||
ctl_fg = colors.gray
|
||||
cur_fg_bg = blk_brn
|
||||
cur_lu = colors.black
|
||||
end
|
||||
|
||||
local _y = ((i - 1) * 5) + 1
|
||||
|
||||
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
|
||||
|
||||
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
rate_limits[i] = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(ctl_fg,colors.white)}
|
||||
local lim = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=lim_fg_bg}
|
||||
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t",width=4,height=1}
|
||||
|
||||
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
|
||||
rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)
|
||||
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(cur_lu,cur_lu),width=14,fg_bg=cur_fg_bg}
|
||||
|
||||
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(colors.black,colors.black),width=14,fg_bg=cpair(colors.black,colors.brown)}
|
||||
if i <= facility.num_units then
|
||||
rate_limits[i] = lim
|
||||
rate_limits[i].register(unit.unit_ps, "max_burn", rate_limits[i].set_max)
|
||||
rate_limits[i].register(unit.unit_ps, "burn_limit", rate_limits[i].set_value)
|
||||
|
||||
cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
|
||||
cur_burn.register(unit.unit_ps, "act_burn_rate", cur_burn.update)
|
||||
else
|
||||
lim.disable()
|
||||
end
|
||||
end
|
||||
|
||||
-------------------
|
||||
-- unit statuses --
|
||||
-------------------
|
||||
|
||||
local stat_div = Div{parent=proc,width=38,height=19,x=57,y=6}
|
||||
local stat_div = Div{parent=proc,width=22,height=24,x=57,y=6}
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
for i = 1, 4 do
|
||||
local tag_fg_bg = gry_wht
|
||||
local ind_fg_bg = style.lg_white
|
||||
local ind_off = colors.lightGray
|
||||
|
||||
if i <= facility.num_units then
|
||||
tag_fg_bg = cpair(colors.black, colors.cyan)
|
||||
ind_fg_bg = bw_fg_bg
|
||||
ind_off = colors.gray
|
||||
end
|
||||
|
||||
local _y = ((i - 1) * 5) + 1
|
||||
|
||||
local unit_tag = Div{parent=stat_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
local unit_tag = Div{parent=stat_div,x=1,y=_y,width=8,height=4,fg_bg=tag_fg_bg}
|
||||
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Status",width=7,height=2}
|
||||
|
||||
local lights = Div{parent=stat_div,x=9,y=_y,width=12,height=4,fg_bg=bw_fg_bg}
|
||||
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,colors.gray)}
|
||||
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local lights = Div{parent=stat_div,x=9,y=_y,width=14,height=4,fg_bg=ind_fg_bg}
|
||||
local ready = IndicatorLight{parent=lights,x=2,y=2,label="Ready",colors=cpair(colors.green,ind_off)}
|
||||
local degraded = IndicatorLight{parent=lights,x=2,y=3,label="Degraded",colors=cpair(colors.red,ind_off),flash=true,period=period.BLINK_250_MS}
|
||||
|
||||
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
|
||||
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
|
||||
if i <= facility.num_units then
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
|
||||
ready.register(unit.unit_ps, "U_AutoReady", ready.update)
|
||||
degraded.register(unit.unit_ps, "U_AutoDegraded", degraded.update)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------------
|
||||
@@ -195,18 +241,18 @@ local function new_view(root, x, y)
|
||||
-------------------------
|
||||
|
||||
local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" }
|
||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.purple,colors.black),radio_bg=colors.gray}
|
||||
local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.gray,colors.white),select_color=colors.purple}
|
||||
|
||||
mode.register(facility.ps, "process_mode", mode.set_value)
|
||||
|
||||
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=31,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
|
||||
|
||||
stat_line_1.register(facility.ps, "status_line_1", stat_line_1.set_value)
|
||||
stat_line_2.register(facility.ps, "status_line_2", stat_line_2.set_value)
|
||||
|
||||
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=gry_wht}
|
||||
|
||||
-- save the automatic process control configuration without starting
|
||||
local function _save_cfg()
|
||||
@@ -261,6 +307,60 @@ local function new_view(root, x, y)
|
||||
for i = 1, #rate_limits do rate_limits[i].enable() end
|
||||
end
|
||||
end)
|
||||
|
||||
------------------------------
|
||||
-- waste production control --
|
||||
------------------------------
|
||||
|
||||
local waste_status = Div{parent=proc,width=24,height=4,x=57,y=1,}
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
local unit = units[i] ---@type ioctl_unit
|
||||
|
||||
TextBox{parent=waste_status,y=i,text="U"..i.." Waste",width=8,height=1}
|
||||
local a_waste = IndicatorLight{parent=waste_status,x=10,y=i,label="Auto",colors=ind_wht}
|
||||
local waste_m = StateIndicator{parent=waste_status,x=17,y=i,states=style.waste.states_abbrv,value=1,min_width=6}
|
||||
|
||||
a_waste.register(unit.unit_ps, "U_AutoWaste", a_waste.update)
|
||||
waste_m.register(unit.unit_ps, "U_WasteProduct", waste_m.update)
|
||||
end
|
||||
|
||||
local waste_sel = Div{parent=proc,width=21,height=24,x=81,y=1}
|
||||
|
||||
TextBox{parent=waste_sel,text=" ",width=21,height=1,x=1,y=1,fg_bg=blk_brn}
|
||||
TextBox{parent=waste_sel,text="WASTE PRODUCTION",alignment=ALIGN.CENTER,width=21,height=1,x=1,y=2,fg_bg=cpair(colors.lightGray,colors.brown)}
|
||||
|
||||
local rect = Rectangle{parent=waste_sel,border=border(1,colors.brown,true),width=21,height=22,x=1,y=3}
|
||||
local status = StateIndicator{parent=rect,x=2,y=1,states=style.waste.states,value=1,min_width=17}
|
||||
|
||||
status.register(facility.ps, "current_waste_product", status.update)
|
||||
|
||||
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(colors.gray,colors.white),select_color=colors.brown}
|
||||
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,colors.black)}
|
||||
|
||||
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)
|
||||
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
|
||||
|
||||
local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=ind_wht}
|
||||
|
||||
fb_active.register(facility.ps, "pu_fallback_active", fb_active.update)
|
||||
|
||||
TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label}
|
||||
local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
|
||||
TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label}
|
||||
local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
|
||||
TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label}
|
||||
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12d",value=0,lu_colors=lu_cpair,fg_bg=bw_fg_bg,width=17}
|
||||
|
||||
pu_rate.register(facility.ps, "pu_rate", pu_rate.update)
|
||||
po_rate.register(facility.ps, "po_rate", po_rate.update)
|
||||
am_rate.register(facility.ps, "am_rate", am_rate.update)
|
||||
|
||||
local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17}
|
||||
|
||||
sna_count.register(facility.ps, "sna_count", sna_count.update)
|
||||
end
|
||||
|
||||
return new_view
|
||||
@@ -14,6 +14,9 @@ local StateIndicator = require("graphics.elements.indicators.state")
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- create new reactor view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
@@ -22,9 +25,6 @@ local border = core.border
|
||||
local function new_view(root, x, y, ps)
|
||||
local reactor = Rectangle{parent=root,border=border(1, colors.gray, true),width=30,height=7,x=x,y=y}
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
|
||||
local status = StateIndicator{parent=reactor,x=6,y=1,states=style.reactor.states,value=1,min_width=16}
|
||||
local core_temp = DataIndicator{parent=reactor,x=2,y=3,lu_colors=lu_col,label="Core Temp:",unit="K",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
local burn_r = DataIndicator{parent=reactor,x=2,y=4,lu_colors=lu_col,label="Burn Rate:",unit="mB/t",format="%10.2f",value=0,width=26,fg_bg=text_fg_bg}
|
||||
|
||||
@@ -15,16 +15,16 @@ local VerticalBar = require("graphics.elements.indicators.vbar")
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local text_fg_bg = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- new turbine view
|
||||
---@param root graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param ps psil ps interface
|
||||
local function new_view(root, x, y, ps)
|
||||
local turbine = Rectangle{parent=root,border=border(1, colors.gray, true),width=23,height=7,x=x,y=y}
|
||||
|
||||
local text_fg_bg = cpair(colors.black, colors.lightGray)
|
||||
local lu_col = cpair(colors.gray, colors.gray)
|
||||
local turbine = Rectangle{parent=root,border=border(1,colors.gray,true),width=23,height=7,x=x,y=y}
|
||||
|
||||
local status = StateIndicator{parent=turbine,x=7,y=1,states=style.turbine.states,value=1,min_width=12}
|
||||
local prod_rate = PowerIndicator{parent=turbine,x=5,y=3,lu_colors=lu_col,label="",format="%10.2f",value=0,rate=true,width=16,fg_bg=text_fg_bg}
|
||||
@@ -32,7 +32,7 @@ local function new_view(root, x, y, ps)
|
||||
|
||||
status.register(ps, "computed_status", status.update)
|
||||
prod_rate.register(ps, "prod_rate", function (val) prod_rate.update(util.joules_to_fe(val)) end)
|
||||
flow_rate.register(ps, "flow_rate", flow_rate.update)
|
||||
flow_rate.register(ps, "steam_input_rate", flow_rate.update)
|
||||
|
||||
local steam = VerticalBar{parent=turbine,x=2,y=1,fg_bg=cpair(colors.white,colors.gray),height=4,width=1}
|
||||
local energy = VerticalBar{parent=turbine,x=3,y=1,fg_bg=cpair(colors.green,colors.gray),height=4,width=1}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
-- Reactor Unit SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
@@ -26,35 +28,24 @@ local PushButton = require("graphics.elements.controls.push_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local lu_cpair = style.lu_colors
|
||||
local hzd_fg_bg = style.hzd_fg_bg
|
||||
local dis_colors = style.dis_colors
|
||||
|
||||
local waste_opts = {
|
||||
{
|
||||
text = "Auto",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.white, colors.gray)
|
||||
},
|
||||
{
|
||||
text = "Pu",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.green)
|
||||
},
|
||||
{
|
||||
text = "Po",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.cyan)
|
||||
},
|
||||
{
|
||||
text = "AM",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.purple)
|
||||
}
|
||||
}
|
||||
local gry_wht = style.gray_white
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_yel = style.ind_yel
|
||||
local ind_red = style.ind_red
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
local period = core.flasher.PERIOD
|
||||
|
||||
-- create a unit view
|
||||
---@param parent graphics_element parent
|
||||
@@ -62,17 +53,16 @@ local waste_opts = {
|
||||
local function init(parent, id)
|
||||
local unit = iocontrol.get_db().units[id] ---@type ioctl_unit
|
||||
local f_ps = iocontrol.get_db().facility.ps
|
||||
|
||||
local main = Div{parent=parent,x=1,y=1}
|
||||
|
||||
if unit == nil then return main end
|
||||
|
||||
local u_ps = unit.unit_ps
|
||||
local b_ps = unit.boiler_ps_tbl
|
||||
local t_ps = unit.turbine_ps_tbl
|
||||
|
||||
local main = Div{parent=parent,x=1,y=1}
|
||||
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local hzd_fg_bg = cpair(colors.white, colors.gray)
|
||||
local lu_cpair = cpair(colors.gray, colors.gray)
|
||||
TextBox{parent=main,text="Reactor Unit #" .. id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
-----------------------------
|
||||
-- main stats and core map --
|
||||
@@ -108,7 +98,7 @@ local function init(parent, id)
|
||||
waste.register(u_ps, "waste_fill", waste.update)
|
||||
|
||||
ccool.register(u_ps, "ccool_type", function (type)
|
||||
if type == "mekanism:sodium" then
|
||||
if type == types.FLUID.SODIUM then
|
||||
ccool.recolor(cpair(colors.lightBlue, colors.gray))
|
||||
else
|
||||
ccool.recolor(cpair(colors.blue, colors.gray))
|
||||
@@ -116,7 +106,7 @@ local function init(parent, id)
|
||||
end)
|
||||
|
||||
hcool.register(u_ps, "hcool_type", function (type)
|
||||
if type == "mekanism:superheated_sodium" then
|
||||
if type == types.FLUID.SUPERHEATED_SODIUM then
|
||||
hcool.recolor(cpair(colors.orange, colors.gray))
|
||||
else
|
||||
hcool.recolor(cpair(colors.white, colors.gray))
|
||||
@@ -144,8 +134,8 @@ local function init(parent, id)
|
||||
-------------------
|
||||
|
||||
local u_stat = Rectangle{parent=main,border=border(1,colors.gray,true),thin=true,width=33,height=4,x=46,y=3,fg_bg=bw_fg_bg}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
|
||||
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=bw_fg_bg}
|
||||
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data...",width=33,height=1,alignment=ALIGN.CENTER,fg_bg=gry_wht}
|
||||
|
||||
stat_line_1.register(u_ps, "U_StatusLine1", stat_line_1.set_value)
|
||||
stat_line_2.register(u_ps, "U_StatusLine2", stat_line_2.set_value)
|
||||
@@ -160,7 +150,7 @@ local function init(parent, id)
|
||||
|
||||
-- connectivity
|
||||
local plc_online = IndicatorLight{parent=annunciator,label="PLC Online",colors=cpair(colors.green,colors.red)}
|
||||
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=cpair(colors.white,colors.gray)}
|
||||
local plc_hbeat = IndicatorLight{parent=annunciator,label="PLC Heartbeat",colors=ind_wht}
|
||||
local rad_mon = TriIndicatorLight{parent=annunciator,label="Radiation Monitor",c1=colors.gray,c2=colors.yellow,c3=colors.green}
|
||||
|
||||
plc_online.register(u_ps, "PLCOnline", plc_online.update)
|
||||
@@ -170,25 +160,25 @@ local function init(parent, id)
|
||||
annunciator.line_break()
|
||||
|
||||
-- operating state
|
||||
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=cpair(colors.green,colors.gray)}
|
||||
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=cpair(colors.white,colors.gray)}
|
||||
local r_active = IndicatorLight{parent=annunciator,label="Active",colors=ind_grn}
|
||||
local r_auto = IndicatorLight{parent=annunciator,label="Automatic Control",colors=ind_wht}
|
||||
|
||||
r_active.register(u_ps, "status", r_active.update)
|
||||
r_auto.register(u_ps, "AutoControl", r_auto.update)
|
||||
|
||||
-- main unit transient/warning annunciator panel
|
||||
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local r_ascrm = IndicatorLight{parent=annunciator,label="Auto Reactor SCRAM",colors=cpair(colors.red,colors.gray)}
|
||||
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=cpair(colors.red,colors.gray)}
|
||||
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=cpair(colors.red,colors.gray)}
|
||||
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=cpair(colors.yellow,colors.gray)}
|
||||
local r_scram = IndicatorLight{parent=annunciator,label="Reactor SCRAM",colors=ind_red}
|
||||
local r_mscrm = IndicatorLight{parent=annunciator,label="Manual Reactor SCRAM",colors=ind_red}
|
||||
local r_ascrm = IndicatorLight{parent=annunciator,label="Auto Reactor SCRAM",colors=ind_red}
|
||||
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=ind_yel}
|
||||
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=ind_red}
|
||||
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=ind_yel}
|
||||
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
|
||||
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=ind_red}
|
||||
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=ind_yel}
|
||||
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel}
|
||||
local r_wloc = IndicatorLight{parent=annunciator,label="Waste Line Occlusion",colors=ind_yel}
|
||||
local r_hsrt = IndicatorLight{parent=annunciator,label="Startup Rate High",colors=ind_yel}
|
||||
|
||||
r_scram.register(u_ps, "ReactorSCRAM", r_scram.update)
|
||||
r_mscrm.register(u_ps, "ManualReactorSCRAM", r_mscrm.update)
|
||||
@@ -205,19 +195,19 @@ local function init(parent, id)
|
||||
|
||||
-- RPS annunciator panel
|
||||
|
||||
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=8}
|
||||
TextBox{parent=main,text="REACTOR PROTECTION SYSTEM",fg_bg=cpair(colors.black,colors.cyan),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=8}
|
||||
local rps = Rectangle{parent=main,border=border(1,colors.cyan,true),thin=true,width=33,height=12,x=46,y=9}
|
||||
local rps_annunc = Div{parent=rps,width=31,height=10,x=2,y=1}
|
||||
|
||||
local rps_trp = IndicatorLight{parent=rps_annunc,label="RPS Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local rps_dmg = IndicatorLight{parent=rps_annunc,label="Damage Level High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local rps_exh = IndicatorLight{parent=rps_annunc,label="Excess Heated Coolant",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_exw = IndicatorLight{parent=rps_annunc,label="Excess Waste",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_tmp = IndicatorLight{parent=rps_annunc,label="Core Temperature High",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local rps_nof = IndicatorLight{parent=rps_annunc,label="No Fuel",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_loc = IndicatorLight{parent=rps_annunc,label="Coolant Level Low Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local rps_flt = IndicatorLight{parent=rps_annunc,label="PPM Fault",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
local rps_trp = IndicatorLight{parent=rps_annunc,label="RPS Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local rps_dmg = IndicatorLight{parent=rps_annunc,label="Damage Level High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local rps_exh = IndicatorLight{parent=rps_annunc,label="Excess Heated Coolant",colors=ind_yel}
|
||||
local rps_exw = IndicatorLight{parent=rps_annunc,label="Excess Waste",colors=ind_yel}
|
||||
local rps_tmp = IndicatorLight{parent=rps_annunc,label="Core Temperature High",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
local rps_nof = IndicatorLight{parent=rps_annunc,label="No Fuel",colors=ind_yel}
|
||||
local rps_loc = IndicatorLight{parent=rps_annunc,label="Coolant Level Low Low",colors=ind_yel}
|
||||
local rps_flt = IndicatorLight{parent=rps_annunc,label="PPM Fault",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
local rps_tmo = IndicatorLight{parent=rps_annunc,label="Connection Timeout",colors=ind_yel,flash=true,period=period.BLINK_500_MS}
|
||||
local rps_sfl = IndicatorLight{parent=rps_annunc,label="System Failure",colors=cpair(colors.orange,colors.gray),flash=true,period=period.BLINK_500_MS}
|
||||
|
||||
rps_trp.register(u_ps, "rps_tripped", rps_trp.update)
|
||||
@@ -233,17 +223,17 @@ local function init(parent, id)
|
||||
|
||||
-- cooling annunciator panel
|
||||
|
||||
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=22}
|
||||
TextBox{parent=main,text="REACTOR COOLANT SYSTEM",fg_bg=cpair(colors.black,colors.blue),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=22}
|
||||
local rcs = Rectangle{parent=main,border=border(1,colors.blue,true),thin=true,width=33,height=24,x=46,y=23}
|
||||
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=3,y=1}
|
||||
local rcs_tags = Div{parent=rcs,width=2,height=16,x=1,y=7}
|
||||
|
||||
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=ind_yel}
|
||||
local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.green}
|
||||
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
|
||||
local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=ind_yel}
|
||||
local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=ind_yel}
|
||||
local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=ind_yel}
|
||||
local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=ind_yel}
|
||||
|
||||
c_flt.register(u_ps, "RCSFault", c_flt.update)
|
||||
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
|
||||
@@ -266,11 +256,11 @@ local function init(parent, id)
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
TextBox{parent=rcs_tags,x=1,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
|
||||
b1_wll.register(b_ps[1], "WasterLevelLow", b1_wll.update)
|
||||
local b1_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
||||
b1_wll.register(b_ps[1], "WaterLevelLow", b1_wll.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="B1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local b1_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
||||
b1_hr.register(b_ps[1], "HeatingRateLow", b1_hr.update)
|
||||
end
|
||||
if unit.num_boilers > 1 then
|
||||
@@ -282,11 +272,11 @@ local function init(parent, id)
|
||||
end
|
||||
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=cpair(colors.red,colors.gray)}
|
||||
b2_wll.register(b_ps[2], "WasterLevelLow", b2_wll.update)
|
||||
local b2_wll = IndicatorLight{parent=rcs_annunc,label="Water Level Low",colors=ind_red}
|
||||
b2_wll.register(b_ps[2], "WaterLevelLow", b2_wll.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="B2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=cpair(colors.yellow,colors.gray)}
|
||||
local b2_hr = IndicatorLight{parent=rcs_annunc,label="Heating Rate Low",colors=ind_yel}
|
||||
b2_hr.register(b_ps[2], "HeatingRateLow", b2_hr.update)
|
||||
end
|
||||
|
||||
@@ -299,15 +289,15 @@ local function init(parent, id)
|
||||
t1_sdo.register(t_ps[1], "SteamDumpOpen", t1_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
|
||||
t1_tos.register(t_ps[1], "TurbineOverSpeed", t1_tos.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local t1_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
|
||||
t1_gtrp.register(t_ps[1], "GeneratorTrip", t1_gtrp.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local t1_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
t1_trp.register(t_ps[1], "TurbineTrip", t1_trp.update)
|
||||
|
||||
if unit.num_turbines > 1 then
|
||||
@@ -320,15 +310,15 @@ local function init(parent, id)
|
||||
t2_sdo.register(t_ps[2], "SteamDumpOpen", t2_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
|
||||
t2_tos.register(t_ps[2], "TurbineOverSpeed", t2_tos.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local t2_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
|
||||
t2_gtrp.register(t_ps[2], "GeneratorTrip", t2_gtrp.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local t2_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
t2_trp.register(t_ps[2], "TurbineTrip", t2_trp.update)
|
||||
end
|
||||
|
||||
@@ -340,15 +330,15 @@ local function init(parent, id)
|
||||
t3_sdo.register(t_ps[3], "SteamDumpOpen", t3_sdo.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
|
||||
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=ind_red}
|
||||
t3_tos.register(t_ps[3], "TurbineOverSpeed", t3_tos.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=cpair(colors.yellow,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local t3_gtrp = IndicatorLight{parent=rcs_annunc,label="Generator Trip",colors=ind_yel,flash=true,period=period.BLINK_250_MS}
|
||||
t3_gtrp.register(t_ps[3], "GeneratorTrip", t3_gtrp.update)
|
||||
|
||||
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
|
||||
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
|
||||
local t3_trp = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=ind_red,flash=true,period=period.BLINK_250_MS}
|
||||
t3_trp.register(t_ps[3], "TurbineTrip", t3_trp.update)
|
||||
end
|
||||
|
||||
@@ -356,14 +346,12 @@ local function init(parent, id)
|
||||
-- reactor controls --
|
||||
----------------------
|
||||
|
||||
local dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=cpair(colors.gray,colors.white)}
|
||||
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
|
||||
local burn_control = Div{parent=main,x=12,y=28,width=19,height=3,fg_bg=gry_wht}
|
||||
local burn_rate = SpinboxNumeric{parent=burn_control,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=gry_wht,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=burn_control,x=9,y=2,text="mB/t"}
|
||||
|
||||
local set_burn = function () unit.set_burn(burn_rate.get_value()) end
|
||||
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=dis_colors,callback=set_burn}
|
||||
local set_burn_btn = PushButton{parent=burn_control,x=14,y=2,text="SET",min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=dis_colors,callback=set_burn}
|
||||
|
||||
burn_rate.register(u_ps, "burn_rate", burn_rate.set_value)
|
||||
burn_rate.register(u_ps, "max_burn", burn_rate.set_max)
|
||||
@@ -394,11 +382,11 @@ local function init(parent, id)
|
||||
|
||||
reset.register(u_ps, "rps_tripped", function (active) if active then reset.enable() else reset.disable() end end)
|
||||
|
||||
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=TEXT_ALIGN.CENTER,width=33,height=1,x=46,y=48}
|
||||
TextBox{parent=main,text="WASTE PROCESSING",fg_bg=cpair(colors.black,colors.brown),alignment=ALIGN.CENTER,width=33,height=1,x=46,y=48}
|
||||
local waste_proc = Rectangle{parent=main,border=border(1,colors.brown,true),thin=true,width=33,height=3,x=46,y=49}
|
||||
local waste_div = Div{parent=waste_proc,x=2,y=1,width=31,height=1}
|
||||
|
||||
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=waste_opts,callback=unit.set_waste,min_width=6}
|
||||
local waste_mode = MultiButton{parent=waste_div,x=1,y=1,options=style.waste.unit_opts,callback=unit.set_waste,min_width=6}
|
||||
|
||||
waste_mode.register(u_ps, "U_WasteMode", waste_mode.set_value)
|
||||
|
||||
@@ -482,20 +470,20 @@ local function init(parent, id)
|
||||
-- automatic control settings --
|
||||
--------------------------------
|
||||
|
||||
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=TEXT_ALIGN.CENTER,width=13,height=1,x=32,y=36}
|
||||
TextBox{parent=main,text="AUTO CTRL",fg_bg=cpair(colors.black,colors.purple),alignment=ALIGN.CENTER,width=13,height=1,x=32,y=36}
|
||||
local auto_ctl = Rectangle{parent=main,border=border(1,colors.purple,true),thin=true,width=13,height=15,x=32,y=37}
|
||||
local auto_div = Div{parent=auto_ctl,width=13,height=15,x=1,y=1}
|
||||
|
||||
local ctl_opts = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" }
|
||||
|
||||
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.blue,colors.white),radio_bg=colors.gray}
|
||||
local group = RadioButton{parent=auto_div,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.gray,colors.white),select_color=colors.purple}
|
||||
|
||||
group.register(u_ps, "auto_group_id", function (gid) group.set_value(gid + 1) end)
|
||||
|
||||
auto_div.line_break()
|
||||
|
||||
local function set_group() unit.set_group(group.get_value() - 1) end
|
||||
local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),dis_fg_bg=cpair(colors.gray,colors.white),callback=set_group}
|
||||
local set_grp_btn = PushButton{parent=auto_div,text="SET",x=4,min_width=5,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=style.wh_gray,dis_fg_bg=gry_wht,callback=set_group}
|
||||
|
||||
auto_div.line_break()
|
||||
|
||||
@@ -506,8 +494,8 @@ local function init(parent, id)
|
||||
|
||||
auto_div.line_break()
|
||||
|
||||
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=cpair(colors.green,colors.gray)}
|
||||
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_1000_MS}
|
||||
local a_rdy = IndicatorLight{parent=auto_div,label="Ready",x=2,colors=ind_grn}
|
||||
local a_stb = IndicatorLight{parent=auto_div,label="Standby",x=2,colors=ind_wht,flash=true,period=period.BLINK_1000_MS}
|
||||
|
||||
a_rdy.register(u_ps, "U_AutoReady", a_rdy.update)
|
||||
|
||||
|
||||
223
coordinator/ui/components/unit_flow.lua
Normal file
223
coordinator/ui/components/unit_flow.lua
Normal file
@@ -0,0 +1,223 @@
|
||||
--
|
||||
-- Basic Unit Flow Overview
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local sprintf = util.sprintf
|
||||
|
||||
local border = core.border
|
||||
local pipe = core.pipe
|
||||
|
||||
local wh_gray = style.wh_gray
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local text_c = style.text_colors
|
||||
local lu_c = style.lu_colors
|
||||
local lg_gray = style.lg_gray
|
||||
|
||||
local ind_grn = style.ind_grn
|
||||
local ind_wht = style.ind_wht
|
||||
|
||||
-- make a new unit flow window
|
||||
---@param parent graphics_element parent
|
||||
---@param x integer top left x
|
||||
---@param y integer top left y
|
||||
---@param wide boolean whether to render wide version
|
||||
---@param unit ioctl_unit unit database entry
|
||||
local function make(parent, x, y, wide, unit)
|
||||
local height = 16
|
||||
|
||||
local v_start = 1 + ((unit.unit_id - 1) * 5)
|
||||
local prv_start = 1 + ((unit.unit_id - 1) * 3)
|
||||
local v_fields = { "pu", "po", "pl", "am" }
|
||||
local v_names = {
|
||||
sprintf("PV%02d-PU", v_start),
|
||||
sprintf("PV%02d-PO", v_start + 1),
|
||||
sprintf("PV%02d-PL", v_start + 2),
|
||||
sprintf("PV%02d-AM", v_start + 3),
|
||||
sprintf("PRV%02d", prv_start),
|
||||
sprintf("PRV%02d", prv_start + 1),
|
||||
sprintf("PRV%02d", prv_start + 2)
|
||||
}
|
||||
|
||||
assert(parent.get_height() >= (y + height), "flow display not of sufficient vertical resolution (add an additional row of monitors) " .. y .. "," .. parent.get_height())
|
||||
|
||||
local function _wide(a, b) return util.trinary(wide, a, b) end
|
||||
|
||||
-- bounding box div
|
||||
local root = Div{parent=parent,x=x,y=y,width=_wide(136, 114),height=height}
|
||||
|
||||
------------------
|
||||
-- COOLING LOOP --
|
||||
------------------
|
||||
|
||||
local reactor = Rectangle{parent=root,x=1,y=1,border=border(1,colors.gray,true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=reactor,y=1,text="FISSION REACTOR",alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=reactor,y=3,text="UNIT #"..unit.unit_id,alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=root,x=19,y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
TextBox{parent=root,x=3,y=5,text="\x19",width=1,height=1,fg_bg=lg_gray}
|
||||
|
||||
local rc_pipes = {}
|
||||
|
||||
local emc_x = 42 -- emergency coolant connection x point
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true))
|
||||
table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true))
|
||||
table.insert(rc_pipes, pipe(_wide(46 ,39), 1, _wide(72,58), 1, colors.blue, true))
|
||||
table.insert(rc_pipes, pipe(_wide(46,39), 3, _wide(72,58), 3, colors.white, true))
|
||||
else
|
||||
emc_x = 3
|
||||
table.insert(rc_pipes, pipe(0, 1, _wide(72,58), 1, colors.blue, true))
|
||||
table.insert(rc_pipes, pipe(0, 3, _wide(72,58), 3, colors.white, true))
|
||||
end
|
||||
|
||||
if unit.has_tank then
|
||||
table.insert(rc_pipes, pipe(emc_x, 1, emc_x, 0, colors.blue, true, true))
|
||||
end
|
||||
|
||||
local prv_yo = math.max(3 - unit.num_turbines, 0)
|
||||
for i = 1, unit.num_turbines do
|
||||
local py = 2 * (i - 1) + prv_yo
|
||||
table.insert(rc_pipes, pipe(_wide(92, 78), py, _wide(104, 83), py, colors.white, true))
|
||||
end
|
||||
|
||||
PipeNetwork{parent=root,x=20,y=1,pipes=rc_pipes,bg=colors.lightGray}
|
||||
|
||||
if unit.num_boilers > 0 then
|
||||
local cc_rate = DataIndicator{parent=root,x=_wide(25,22),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local hc_rate = DataIndicator{parent=root,x=_wide(25,22),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
|
||||
cc_rate.register(unit.unit_ps, "boiler_boil_sum", function (sum) cc_rate.update(sum * 10) end)
|
||||
hc_rate.register(unit.unit_ps, "heating_rate", hc_rate.update)
|
||||
|
||||
local boiler = Rectangle{parent=root,x=_wide(47,40),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=boiler,y=1,text="THERMO-ELECTRIC",alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=boiler,y=3,text=util.trinary(unit.num_boilers>1,"BOILERS","BOILER"),alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=root,x=_wide(47,40),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
TextBox{parent=root,x=_wide(65,58),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
|
||||
local wt_rate = DataIndicator{parent=root,x=_wide(71,61),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local st_rate = DataIndicator{parent=root,x=_wide(71,61),y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
|
||||
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
|
||||
st_rate.register(unit.unit_ps, "boiler_boil_sum", st_rate.update)
|
||||
else
|
||||
local wt_rate = DataIndicator{parent=root,x=28,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
local st_rate = DataIndicator{parent=root,x=28,y=5,lu_colors=lu_c,label="",unit="mB/t",format="%11.0f",value=0,commas=true,width=16,fg_bg=bw_fg_bg}
|
||||
|
||||
wt_rate.register(unit.unit_ps, "turbine_flow_sum", wt_rate.update)
|
||||
st_rate.register(unit.unit_ps, "heating_rate", st_rate.update)
|
||||
end
|
||||
|
||||
local turbine = Rectangle{parent=root,x=_wide(93,79),y=1,border=border(1, colors.gray, true),width=19,height=5,fg_bg=wh_gray}
|
||||
TextBox{parent=turbine,y=1,text="STEAM TURBINE",alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=turbine,y=3,text=util.trinary(unit.num_turbines>1,"GENERATORS","GENERATOR"),alignment=ALIGN.CENTER,height=1}
|
||||
TextBox{parent=root,x=_wide(93,79),y=2,text="\x1b \x80 \x1a",width=1,height=3,fg_bg=lg_gray}
|
||||
|
||||
for i = 1, unit.num_turbines do
|
||||
local ry = 1 + (2 * (i - 1)) + prv_yo
|
||||
TextBox{parent=root,x=_wide(125,103),y=ry,text="\x10\x11\x7f",fg_bg=text_c,width=3,height=1}
|
||||
local state = TriIndicatorLight{parent=root,x=_wide(129,107),y=ry,label=v_names[i+4],c1=colors.gray,c2=colors.yellow,c3=colors.red}
|
||||
state.register(unit.turbine_ps_tbl[i], "SteamDumpOpen", state.update)
|
||||
end
|
||||
|
||||
----------------------
|
||||
-- WASTE PROCESSING --
|
||||
----------------------
|
||||
|
||||
local waste = Div{parent=root,x=3,y=6}
|
||||
|
||||
local waste_pipes = {
|
||||
pipe(0, 0, _wide(19, 16), 1, colors.brown, true),
|
||||
pipe(_wide(14, 13), 1, _wide(19, 17), 5, colors.brown, true),
|
||||
pipe(_wide(22, 19), 1, _wide(49, 45), 1, colors.brown, true),
|
||||
pipe(_wide(22, 19), 5, _wide(28, 24), 5, colors.brown, true),
|
||||
|
||||
pipe(_wide(64, 53), 1, _wide(95, 81), 1, colors.green, true),
|
||||
|
||||
pipe(_wide(48, 43), 4, _wide(71, 61), 4, colors.cyan, true),
|
||||
pipe(_wide(66, 57), 4, _wide(71, 61), 8, colors.cyan, true),
|
||||
pipe(_wide(74, 63), 4, _wide(95, 81), 4, colors.cyan, true),
|
||||
pipe(_wide(74, 63), 8, _wide(133, 111), 8, colors.cyan, true),
|
||||
|
||||
pipe(_wide(108, 94), 1, _wide(132, 110), 6, colors.black, true, true),
|
||||
pipe(_wide(108, 94), 4, _wide(111, 95), 1, colors.black, true, true),
|
||||
pipe(_wide(132, 110), 6, _wide(130, 108), 6, colors.black, true, true)
|
||||
}
|
||||
|
||||
PipeNetwork{parent=waste,x=1,y=1,pipes=waste_pipes,bg=colors.lightGray}
|
||||
|
||||
local function _valve(vx, vy, n)
|
||||
TextBox{parent=waste,x=vx,y=vy,text="\x10\x11",fg_bg=text_c,width=2,height=1}
|
||||
local conn = IndicatorLight{parent=waste,x=vx-3,y=vy+1,label=v_names[n],colors=ind_grn}
|
||||
local open = IndicatorLight{parent=waste,x=vx-3,y=vy+2,label="OPEN",colors=ind_wht}
|
||||
conn.register(unit.unit_ps, util.c("V_", v_fields[n], "_conn"), conn.update)
|
||||
open.register(unit.unit_ps, util.c("V_", v_fields[n], "_state"), open.update)
|
||||
end
|
||||
|
||||
local function _machine(mx, my, name)
|
||||
local l = string.len(name) + 2
|
||||
TextBox{parent=waste,x=mx,y=my,text=string.rep("\x8f",l),alignment=ALIGN.CENTER,fg_bg=lg_gray,width=l,height=1}
|
||||
TextBox{parent=waste,x=mx,y=my+1,text=name,alignment=ALIGN.CENTER,fg_bg=wh_gray,width=l,height=1}
|
||||
end
|
||||
|
||||
local waste_rate = DataIndicator{parent=waste,x=1,y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.2f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local pu_rate = DataIndicator{parent=waste,x=_wide(82,70),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local po_rate = DataIndicator{parent=waste,x=_wide(52,45),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local popl_rate = DataIndicator{parent=waste,x=_wide(82,70),y=6,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local poam_rate = DataIndicator{parent=waste,x=_wide(82,70),y=10,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
local spent_rate = DataIndicator{parent=waste,x=_wide(117,99),y=3,lu_colors=lu_c,label="",unit="mB/t",format="%7.3f",value=0,width=12,fg_bg=bw_fg_bg}
|
||||
|
||||
waste_rate.register(unit.unit_ps, "act_burn_rate", waste_rate.update)
|
||||
pu_rate.register(unit.unit_ps, "pu_rate", pu_rate.update)
|
||||
po_rate.register(unit.unit_ps, "po_rate", po_rate.update)
|
||||
popl_rate.register(unit.unit_ps, "po_pl_rate", popl_rate.update)
|
||||
poam_rate.register(unit.unit_ps, "po_am_rate", poam_rate.update)
|
||||
spent_rate.register(unit.unit_ps, "ws_rate", spent_rate.update)
|
||||
|
||||
_valve(_wide(21, 18), 2, 1)
|
||||
_valve(_wide(21, 18), 6, 2)
|
||||
_valve(_wide(73, 62), 5, 3)
|
||||
_valve(_wide(73, 62), 9, 4)
|
||||
|
||||
_machine(_wide(51, 45), 1, "CENTRIFUGE \x1a");
|
||||
_machine(_wide(97, 83), 1, "PRC [Pu] \x1a");
|
||||
_machine(_wide(97, 83), 4, "PRC [Po] \x1a");
|
||||
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
|
||||
|
||||
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=bw_fg_bg}
|
||||
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
|
||||
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c,label="CNT",unit="",format="%2d",value=0,width=7}
|
||||
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17}
|
||||
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17}
|
||||
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c,label="IN",unit="mB/t",format="%9.2f",value=0,width=17}
|
||||
|
||||
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end)
|
||||
sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
|
||||
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
|
||||
sna_max.register(unit.unit_ps, "sna_prod_rate", sna_max.update)
|
||||
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
|
||||
|
||||
return root
|
||||
end
|
||||
|
||||
return make
|
||||
@@ -14,7 +14,7 @@ local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local pipe = core.pipe
|
||||
|
||||
@@ -34,17 +34,17 @@ local function make(parent, x, y, unit)
|
||||
|
||||
if num_boilers == 0 and num_turbines == 1 then
|
||||
height = 9
|
||||
elseif num_boilers == 1 and num_turbines <= 2 then
|
||||
elseif num_boilers <= 1 and num_turbines <= 2 then
|
||||
height = 17
|
||||
end
|
||||
|
||||
assert(parent.height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
assert(parent.get_height() >= (y + height), "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
-- bounding box div
|
||||
local root = Div{parent=parent,x=x,y=y,width=80,height=height}
|
||||
|
||||
-- unit header message
|
||||
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
TextBox{parent=root,text="Unit #"..unit.unit_id,alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
-------------
|
||||
-- REACTOR --
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
local completion = require("cc.completion")
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local print = util.print
|
||||
|
||||
local dialog = {}
|
||||
|
||||
-- ask the user yes or no
|
||||
---@nodiscard
|
||||
---@param question string
|
||||
---@param default boolean
|
||||
---@return boolean|nil
|
||||
function dialog.ask_y_n(question, default)
|
||||
print(question)
|
||||
|
||||
if default == true then
|
||||
print(" (Y/n)? ")
|
||||
else
|
||||
print(" (y/N)? ")
|
||||
end
|
||||
|
||||
local response = read(nil, nil)
|
||||
|
||||
if response == "" then
|
||||
return default
|
||||
elseif response == "Y" or response == "y" then
|
||||
return true
|
||||
elseif response == "N" or response == "n" then
|
||||
return false
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
-- ask the user for an input within a set of options
|
||||
---@nodiscard
|
||||
---@param options table
|
||||
---@param cancel string
|
||||
---@return boolean|string|nil
|
||||
function dialog.ask_options(options, cancel)
|
||||
print("> ")
|
||||
local response = read(nil, nil, function(text) return completion.choice(text, options) end)
|
||||
|
||||
if response == cancel then return false end
|
||||
|
||||
if util.table_contains(options, response) then
|
||||
return response
|
||||
else return nil end
|
||||
end
|
||||
|
||||
return dialog
|
||||
387
coordinator/ui/layout/flow_view.lua
Normal file
387
coordinator/ui/layout/flow_view.lua
Normal file
@@ -0,0 +1,387 @@
|
||||
--
|
||||
-- Flow Monitor GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local unit_flow = require("coordinator.ui.components.unit_flow")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local PipeNetwork = require("graphics.elements.pipenet")
|
||||
local Rectangle = require("graphics.elements.rectangle")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
local HorizontalBar = require("graphics.elements.indicators.hbar")
|
||||
local IndicatorLight = require("graphics.elements.indicators.light")
|
||||
local StateIndicator = require("graphics.elements.indicators.state")
|
||||
|
||||
local CONTAINER_MODE = types.CONTAINER_MODE
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local border = core.border
|
||||
local pipe = core.pipe
|
||||
|
||||
local wh_gray = style.wh_gray
|
||||
local bw_fg_bg = style.bw_fg_bg
|
||||
local text_col = style.text_colors
|
||||
local lu_col = style.lu_colors
|
||||
|
||||
-- create new flow view
|
||||
---@param main graphics_element main displaybox
|
||||
local function init(main)
|
||||
local facility = iocontrol.get_db().facility
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
local tank_defs = facility.tank_defs
|
||||
local tank_list = facility.tank_list
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Facility Coolant and Waste Flow Monitor",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
|
||||
local po_pipes = {}
|
||||
local water_pipes = {}
|
||||
|
||||
-- get the y offset for this unit index
|
||||
---@param idx integer unit index
|
||||
local function y_ofs(idx) return ((idx - 1) * 20) end
|
||||
|
||||
-- determinte facility tank start/end from the definitions list
|
||||
---@param start_idx integer start index of table iteration
|
||||
---@param end_idx integer end index of table iteration
|
||||
local function find_fdef(start_idx, end_idx)
|
||||
local first, last = 4, 0
|
||||
for i = start_idx, end_idx do
|
||||
if tank_defs[i] == 2 then
|
||||
last = i
|
||||
if i < first then first = i end
|
||||
end
|
||||
end
|
||||
return first, last
|
||||
end
|
||||
|
||||
if facility.tank_mode == 0 or facility.tank_mode == 8 then
|
||||
-- (0) tanks belong to reactor units OR (8) 4 total facility tanks (A B C D)
|
||||
for i = 1, facility.num_units do
|
||||
if units[i].has_tank then
|
||||
local y = y_ofs(i)
|
||||
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
|
||||
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
|
||||
|
||||
local u = units[i] ---@type ioctl_unit
|
||||
local x = util.trinary(u.num_boilers == 0, 45, 84)
|
||||
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
|
||||
end
|
||||
end
|
||||
else
|
||||
-- setup connections for units with emergency coolant, always the same
|
||||
for i = 1, #tank_defs do
|
||||
if tank_defs[i] > 0 then
|
||||
local y = y_ofs(i)
|
||||
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(1, y, 21, y, colors.blue, true))
|
||||
else
|
||||
table.insert(water_pipes, pipe(2, y, 2, y + 3, colors.blue, true))
|
||||
table.insert(water_pipes, pipe(2, y, 21, y, colors.blue, true))
|
||||
end
|
||||
|
||||
local u = units[i] ---@type ioctl_unit
|
||||
local x = util.trinary(u.num_boilers == 0, 45, 84)
|
||||
table.insert(water_pipes, pipe(21, y, x, y + 2, colors.blue, true, true))
|
||||
end
|
||||
end
|
||||
|
||||
if facility.tank_mode == 1 then
|
||||
-- (1) 1 total facility tank (A A A A)
|
||||
local first_fdef, last_fdef = find_fdef(1, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 2 then
|
||||
-- (2) 2 total facility tanks (A A A B)
|
||||
local first_fdef, last_fdef = find_fdef(1, math.min(3, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 3 then
|
||||
-- (3) 2 total facility tanks (A A B B)
|
||||
for _, a in pairs({ 1, 3 }) do
|
||||
local b = a + 1
|
||||
if tank_defs[a] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(a), 1, y_ofs(a) + 6, colors.blue, true))
|
||||
if tank_defs[b] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(b) - 13, 1, y_ofs(b), colors.blue, true))
|
||||
end
|
||||
elseif tank_defs[b] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y_ofs(b), 1, y_ofs(b) + 6, colors.blue, true))
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 4 then
|
||||
-- (4) 2 total facility tanks (A B B B)
|
||||
local first_fdef, last_fdef = find_fdef(2, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 5 then
|
||||
-- (5) 3 total facility tanks (A A B C)
|
||||
local first_fdef, last_fdef = find_fdef(1, math.min(2, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 3 or i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 6 then
|
||||
-- (6) 3 total facility tanks (A B B C)
|
||||
local first_fdef, last_fdef = find_fdef(2, math.min(3, #tank_defs))
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 or i == 4 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif facility.tank_mode == 7 then
|
||||
-- (7) 3 total facility tanks (A B C C)
|
||||
local first_fdef, last_fdef = find_fdef(3, #tank_defs)
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
local y = y_ofs(i)
|
||||
if i == 1 or i == 2 then
|
||||
if tank_defs[i] == 2 then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
end
|
||||
elseif i == first_fdef then
|
||||
table.insert(water_pipes, pipe(0, y, 1, y + 5, colors.blue, true))
|
||||
elseif i > first_fdef then
|
||||
if i == last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y, colors.blue, true))
|
||||
elseif i < last_fdef then
|
||||
table.insert(water_pipes, pipe(0, y - 14, 0, y + 5, colors.blue, true))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local flow_x = 3
|
||||
if #water_pipes > 0 then
|
||||
flow_x = 25
|
||||
PipeNetwork{parent=main,x=2,y=3,pipes=water_pipes,bg=colors.lightGray}
|
||||
end
|
||||
|
||||
for i = 1, facility.num_units do
|
||||
local y_offset = y_ofs(i)
|
||||
unit_flow(main, flow_x, 5 + y_offset, #water_pipes == 0, units[i])
|
||||
table.insert(po_pipes, pipe(0, 3 + y_offset, 4, 0, colors.cyan, true, true))
|
||||
end
|
||||
|
||||
PipeNetwork{parent=main,x=139,y=15,pipes=po_pipes,bg=colors.lightGray}
|
||||
|
||||
-----------------
|
||||
-- tank valves --
|
||||
-----------------
|
||||
|
||||
local next_f_id = 1
|
||||
|
||||
for i = 1, #tank_defs do
|
||||
if tank_defs[i] > 0 then
|
||||
local vy = 3 + y_ofs(i)
|
||||
|
||||
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2,height=1}
|
||||
|
||||
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn}
|
||||
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
|
||||
|
||||
conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
|
||||
open.register(units[i].unit_ps, "V_emc_state", open.update)
|
||||
end
|
||||
end
|
||||
|
||||
-------------------
|
||||
-- dynamic tanks --
|
||||
-------------------
|
||||
|
||||
for i = 1, #tank_list do
|
||||
if tank_list[i] > 0 then
|
||||
local id = "U-" .. i
|
||||
local f_id = next_f_id
|
||||
if tank_list[i] == 2 then
|
||||
id = "F-" .. next_f_id
|
||||
next_f_id = next_f_id + 1
|
||||
end
|
||||
|
||||
local y_offset = y_ofs(i)
|
||||
|
||||
local tank = Div{parent=main,x=3,y=7+y_offset,width=20,height=14}
|
||||
|
||||
TextBox{parent=tank,text=" ",height=1,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=tank,text="DYNAMIC TANK "..id,alignment=ALIGN.CENTER,height=1,fg_bg=style.wh_gray}
|
||||
|
||||
local tank_box = Rectangle{parent=tank,border=border(1,colors.gray,true),width=20,height=12}
|
||||
|
||||
local status = StateIndicator{parent=tank_box,x=3,y=1,states=style.dtank.states,value=1,min_width=14}
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=3,text="Fill",height=1,width=10,fg_bg=style.label}
|
||||
local tank_pcnt = DataIndicator{parent=tank_box,x=10,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_col}
|
||||
local tank_amnt = DataIndicator{parent=tank_box,x=2,label="",format="%13d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=16,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=6,text="Water Level",height=1,width=11,fg_bg=style.label}
|
||||
local level = HorizontalBar{parent=tank_box,x=2,y=7,bar_fg_bg=cpair(colors.blue,colors.gray),height=1,width=16}
|
||||
|
||||
TextBox{parent=tank_box,x=2,y=9,text="In/Out Mode",height=1,width=11,fg_bg=style.label}
|
||||
local can_fill = IndicatorLight{parent=tank_box,x=2,y=10,label="FILL",colors=style.ind_wht}
|
||||
local can_empty = IndicatorLight{parent=tank_box,x=10,y=10,label="EMPTY",colors=style.ind_wht}
|
||||
|
||||
local function _can_fill(mode)
|
||||
can_fill.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.FILL))
|
||||
end
|
||||
|
||||
local function _can_empty(mode)
|
||||
can_empty.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.EMPTY))
|
||||
end
|
||||
|
||||
if tank_list[i] == 1 then
|
||||
status.register(units[i].tank_ps_tbl[1], "computed_status", status.update)
|
||||
tank_pcnt.register(units[i].tank_ps_tbl[1], "fill", function (f) tank_pcnt.update(f * 100) end)
|
||||
tank_amnt.register(units[i].tank_ps_tbl[1], "stored", function (sto) tank_amnt.update(sto.amount) end)
|
||||
level.register(units[i].tank_ps_tbl[1], "fill", level.update)
|
||||
can_fill.register(units[i].tank_ps_tbl[1], "container_mode", _can_fill)
|
||||
can_empty.register(units[i].tank_ps_tbl[1], "container_mode", _can_empty)
|
||||
else
|
||||
status.register(facility.tank_ps_tbl[f_id], "computed_status", status.update)
|
||||
tank_pcnt.register(facility.tank_ps_tbl[f_id], "fill", function (f) tank_pcnt.update(f * 100) end)
|
||||
tank_amnt.register(facility.tank_ps_tbl[f_id], "stored", function (sto) tank_amnt.update(sto.amount) end)
|
||||
level.register(facility.tank_ps_tbl[f_id], "fill", level.update)
|
||||
can_fill.register(facility.tank_ps_tbl[f_id], "container_mode", _can_fill)
|
||||
can_empty.register(facility.tank_ps_tbl[f_id], "container_mode", _can_empty)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---------
|
||||
-- SPS --
|
||||
---------
|
||||
|
||||
local sps = Div{parent=main,x=140,y=3,height=12}
|
||||
|
||||
TextBox{parent=sps,text=" ",width=24,height=1,x=1,y=1,fg_bg=style.lg_gray}
|
||||
TextBox{parent=sps,text="SPS",alignment=ALIGN.CENTER,width=24,height=1,fg_bg=wh_gray}
|
||||
|
||||
local sps_box = Rectangle{parent=sps,border=border(1,colors.gray,true),width=24,height=10}
|
||||
|
||||
local status = StateIndicator{parent=sps_box,x=5,y=1,states=style.sps.states,value=1,min_width=14}
|
||||
|
||||
status.register(facility.sps_ps_tbl[1], "computed_status", status.update)
|
||||
|
||||
TextBox{parent=sps_box,x=2,y=3,text="Input Rate",height=1,width=10,fg_bg=style.label}
|
||||
local sps_in = DataIndicator{parent=sps_box,x=2,label="",format="%15.3f",value=0,unit="mB/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg}
|
||||
|
||||
sps_in.register(facility.ps, "po_am_rate", sps_in.update)
|
||||
|
||||
TextBox{parent=sps_box,x=2,y=6,text="Production Rate",height=1,width=15,fg_bg=style.label}
|
||||
local sps_rate = DataIndicator{parent=sps_box,x=2,label="",format="%15d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=20,fg_bg=bw_fg_bg}
|
||||
|
||||
sps_rate.register(facility.sps_ps_tbl[1], "process_rate", function (r) sps_rate.update(r * 1000) end)
|
||||
|
||||
----------------
|
||||
-- statistics --
|
||||
----------------
|
||||
|
||||
TextBox{parent=main,x=145,y=16,text="RAW WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local raw_waste = Rectangle{parent=main,x=145,y=17,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
|
||||
local sum_raw_waste = DataIndicator{parent=raw_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.2f",value=0,width=17}
|
||||
|
||||
sum_raw_waste.register(facility.ps, "burn_sum", sum_raw_waste.update)
|
||||
|
||||
TextBox{parent=main,x=145,y=21,text="PROC. WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local pr_waste = Rectangle{parent=main,x=145,y=22,border=border(1,colors.gray,true),width=19,height=5,thin=true,fg_bg=bw_fg_bg}
|
||||
local pu = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Pu",unit="mB/t",format="%9.3f",value=0,width=17}
|
||||
local po = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="Po",unit="mB/t",format="%9.3f",value=0,width=17}
|
||||
local popl = DataIndicator{parent=pr_waste,lu_colors=lu_col,label="PoPl",unit="mB/t",format="%7.3f",value=0,width=17}
|
||||
|
||||
pu.register(facility.ps, "pu_rate", pu.update)
|
||||
po.register(facility.ps, "po_rate", po.update)
|
||||
popl.register(facility.ps, "po_pl_rate", popl.update)
|
||||
|
||||
TextBox{parent=main,x=145,y=28,text="SPENT WASTE",alignment=ALIGN.CENTER,width=19,height=1,fg_bg=wh_gray}
|
||||
local sp_waste = Rectangle{parent=main,x=145,y=29,border=border(1,colors.gray,true),width=19,height=3,thin=true,fg_bg=bw_fg_bg}
|
||||
local sum_sp_waste = DataIndicator{parent=sp_waste,lu_colors=lu_col,label="SUM",unit="mB/t",format="%8.3f",value=0,width=17}
|
||||
|
||||
sum_sp_waste.register(facility.ps, "spent_waste_rate", sum_sp_waste.update)
|
||||
end
|
||||
|
||||
return init
|
||||
126
coordinator/ui/layout/front_panel.lua
Normal file
126
coordinator/ui/layout/front_panel.lua
Normal file
@@ -0,0 +1,126 @@
|
||||
--
|
||||
-- Coordinator Front Panel GUI
|
||||
--
|
||||
|
||||
local types = require("scada-common.types")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local pgi = require("coordinator.ui.pgi")
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local pkt_entry = require("coordinator.ui.components.pkt_entry")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local TabBar = require("graphics.elements.controls.tabbar")
|
||||
|
||||
local LED = require("graphics.elements.indicators.led")
|
||||
local RGBLED = require("graphics.elements.indicators.ledrgb")
|
||||
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local led_grn = style.led_grn
|
||||
|
||||
-- create new front panel view
|
||||
---@param panel graphics_element main displaybox
|
||||
---@param num_units integer number of units (number of unit monitors)
|
||||
local function init(panel, num_units)
|
||||
local ps = iocontrol.get_db().fp.ps
|
||||
|
||||
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,height=1,fg_bg=style.fp.header}
|
||||
|
||||
local page_div = Div{parent=panel,x=1,y=3}
|
||||
|
||||
--
|
||||
-- system indicators
|
||||
--
|
||||
|
||||
local main_page = Div{parent=page_div,x=1,y=1}
|
||||
|
||||
local system = Div{parent=main_page,width=14,height=17,x=2,y=2}
|
||||
|
||||
local status = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
|
||||
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=led_grn}
|
||||
status.update(true)
|
||||
system.line_break()
|
||||
|
||||
heartbeat.register(ps, "heartbeat", heartbeat.update)
|
||||
|
||||
local modem = LED{parent=system,label="MODEM",colors=led_grn}
|
||||
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,colors.gray}}
|
||||
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
|
||||
system.line_break()
|
||||
|
||||
modem.register(ps, "has_modem", modem.update)
|
||||
network.register(ps, "link_state", network.update)
|
||||
|
||||
local speaker = LED{parent=system,label="SPEAKER",colors=led_grn}
|
||||
speaker.register(ps, "has_speaker", speaker.update)
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local comp_id = util.sprintf("(%d)", os.getComputerID())
|
||||
TextBox{parent=system,x=9,y=4,width=6,height=1,text=comp_id,fg_bg=style.fp_label}
|
||||
|
||||
local monitors = Div{parent=main_page,width=16,height=17,x=18,y=2}
|
||||
|
||||
local main_monitor = LED{parent=monitors,label="MAIN MONITOR",colors=led_grn}
|
||||
main_monitor.register(ps, "main_monitor", main_monitor.update)
|
||||
|
||||
local flow_monitor = LED{parent=monitors,label="FLOW MONITOR",colors=led_grn}
|
||||
flow_monitor.register(ps, "flow_monitor", flow_monitor.update)
|
||||
|
||||
monitors.line_break()
|
||||
|
||||
for i = 1, num_units do
|
||||
local unit_monitor = LED{parent=monitors,label="UNIT "..i.." MONITOR",colors=led_grn}
|
||||
unit_monitor.register(ps, "unit_monitor_" .. i, unit_monitor.update)
|
||||
end
|
||||
|
||||
--
|
||||
-- about footer
|
||||
--
|
||||
|
||||
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp_label}
|
||||
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00",alignment=ALIGN.LEFT,height=1}
|
||||
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00",alignment=ALIGN.LEFT,height=1}
|
||||
|
||||
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
|
||||
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
|
||||
|
||||
--
|
||||
-- page handling
|
||||
--
|
||||
|
||||
-- API page
|
||||
|
||||
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
|
||||
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp_text,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
|
||||
local _ = Div{parent=api_list,height=1,hidden=true} -- padding
|
||||
|
||||
-- assemble page panes
|
||||
|
||||
local panes = { main_page, api_page }
|
||||
|
||||
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
|
||||
|
||||
local tabs = {
|
||||
{ name = "CRD", color = style.fp_text },
|
||||
{ name = "API", color = style.fp_text },
|
||||
}
|
||||
|
||||
TabBar{parent=panel,y=2,tabs=tabs,min_width=9,callback=page_pane.set_value,fg_bg=style.bw_fg_bg}
|
||||
|
||||
-- link pocket API list management to PGI
|
||||
pgi.link_elements(api_list, pkt_entry)
|
||||
end
|
||||
|
||||
return init
|
||||
@@ -2,14 +2,12 @@
|
||||
-- Main SCADA Coordinator GUI
|
||||
--
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local iocontrol = require("coordinator.iocontrol")
|
||||
|
||||
local style = require("coordinator.ui.style")
|
||||
|
||||
local imatrix = require("coordinator.ui.components.imatrix")
|
||||
local process_ctl = require("coordinator.ui.components.processctl")
|
||||
local process_ctl = require("coordinator.ui.components.process_ctl")
|
||||
local unit_overview = require("coordinator.ui.components.unit_overview")
|
||||
|
||||
local core = require("graphics.core")
|
||||
@@ -18,9 +16,7 @@ local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local DataIndicator = require("graphics.elements.indicators.data")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
|
||||
local cpair = core.cpair
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
-- create new main view
|
||||
---@param main graphics_element main displaybox
|
||||
@@ -29,10 +25,10 @@ local function init(main)
|
||||
local units = iocontrol.get_db().units
|
||||
|
||||
-- window header message
|
||||
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=cpair(colors.lightGray, colors.white),width=12,fg_bg=style.header}
|
||||
local header = TextBox{parent=main,y=1,text="Nuclear Generation Facility SCADA Coordinator",alignment=ALIGN.CENTER,height=1,fg_bg=style.header}
|
||||
local ping = DataIndicator{parent=main,x=1,y=1,label="SVTT",format="%d",value=0,unit="ms",lu_colors=style.lg_white,width=12,fg_bg=style.header}
|
||||
-- max length example: "01:23:45 AM - Wednesday, September 28 2022"
|
||||
local datetime = TextBox{parent=main,x=(header.width()-42),y=1,text="",alignment=TEXT_ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
local datetime = TextBox{parent=main,x=(header.get_width()-42),y=1,text="",alignment=ALIGN.RIGHT,width=42,height=1,fg_bg=style.header}
|
||||
|
||||
ping.register(facility.ps, "sv_ping", ping.update)
|
||||
datetime.register(facility.ps, "date_time", datetime.set_value)
|
||||
@@ -45,12 +41,12 @@ local function init(main)
|
||||
-- unit overviews
|
||||
if facility.num_units >= 1 then
|
||||
uo_1 = unit_overview(main, 2, 3, units[1])
|
||||
row_1_height = uo_1.height()
|
||||
row_1_height = uo_1.get_height()
|
||||
end
|
||||
|
||||
if facility.num_units >= 2 then
|
||||
uo_2 = unit_overview(main, 84, 3, units[2])
|
||||
row_1_height = math.max(row_1_height, uo_2.height())
|
||||
row_1_height = math.max(row_1_height, uo_2.get_height())
|
||||
end
|
||||
|
||||
cnc_y_start = cnc_y_start + row_1_height + 1
|
||||
@@ -60,11 +56,11 @@ local function init(main)
|
||||
local row_2_offset = cnc_y_start
|
||||
|
||||
uo_3 = unit_overview(main, 2, row_2_offset, units[3])
|
||||
cnc_y_start = row_2_offset + uo_3.height() + 1
|
||||
cnc_y_start = row_2_offset + uo_3.get_height() + 1
|
||||
|
||||
if facility.num_units == 4 then
|
||||
uo_4 = unit_overview(main, 84, row_2_offset, units[4])
|
||||
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.height() + 1)
|
||||
cnc_y_start = math.max(cnc_y_start, row_2_offset + uo_4.get_height() + 1)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,11 +69,11 @@ local function init(main)
|
||||
cnc_y_start = cnc_y_start
|
||||
|
||||
-- induction matrix and process control interfaces are 24 tall + space needed for divider
|
||||
local cnc_bottom_align_start = main.height() - 26
|
||||
local cnc_bottom_align_start = main.get_height() - 26
|
||||
|
||||
assert(cnc_bottom_align_start >= cnc_y_start, "main display not of sufficient vertical resolution (add an additional row of monitors)")
|
||||
|
||||
TextBox{parent=main,y=cnc_bottom_align_start,text=util.strrep("\x8c", header.width()),alignment=TEXT_ALIGN.CENTER,height=1,fg_bg=cpair(colors.lightGray,colors.gray)}
|
||||
TextBox{parent=main,y=cnc_bottom_align_start,text=string.rep("\x8c", header.get_width()),alignment=ALIGN.CENTER,height=1,fg_bg=style.lg_gray}
|
||||
|
||||
cnc_bottom_align_start = cnc_bottom_align_start + 2
|
||||
|
||||
|
||||
60
coordinator/ui/pgi.lua
Normal file
60
coordinator/ui/pgi.lua
Normal file
@@ -0,0 +1,60 @@
|
||||
--
|
||||
-- Protected Graphics Interface
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local pgi = {}
|
||||
|
||||
local data = {
|
||||
pkt_list = nil, ---@type nil|graphics_element
|
||||
pkt_entry = nil, ---@type function
|
||||
-- session entries
|
||||
s_entries = { pkt = {} }
|
||||
}
|
||||
|
||||
-- link list boxes
|
||||
---@param pkt_list graphics_element pocket list element
|
||||
---@param pkt_entry function pocket entry constructor
|
||||
function pgi.link_elements(pkt_list, pkt_entry)
|
||||
data.pkt_list = pkt_list
|
||||
data.pkt_entry = pkt_entry
|
||||
end
|
||||
|
||||
-- unlink all fields, disabling the PGI
|
||||
function pgi.unlink()
|
||||
data.pkt_list = nil
|
||||
data.pkt_entry = nil
|
||||
end
|
||||
|
||||
-- add a PKT entry to the PKT list
|
||||
---@param session_id integer pocket session
|
||||
function pgi.create_pkt_entry(session_id)
|
||||
if data.pkt_list ~= nil and data.pkt_entry ~= nil then
|
||||
local success, result = pcall(data.pkt_entry, data.pkt_list, session_id)
|
||||
|
||||
if success then
|
||||
data.s_entries.pkt[session_id] = result
|
||||
else
|
||||
log.error(util.c("PGI: failed to create PKT entry (", result, ")"), true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- delete a PKT entry from the PKT list
|
||||
---@param session_id integer pocket session
|
||||
function pgi.delete_pkt_entry(session_id)
|
||||
if data.s_entries.pkt[session_id] ~= nil then
|
||||
local success, result = pcall(data.s_entries.pkt[session_id].delete)
|
||||
data.s_entries.pkt[session_id] = nil
|
||||
|
||||
if not success then
|
||||
log.error(util.c("PGI: failed to delete PKT entry (", result, ")"), true)
|
||||
end
|
||||
else
|
||||
log.debug(util.c("PGI: tried to delete unknown PKT entry ", session_id))
|
||||
end
|
||||
end
|
||||
|
||||
return pgi
|
||||
@@ -10,6 +10,41 @@ local cpair = core.cpair
|
||||
|
||||
-- GLOBAL --
|
||||
|
||||
-- add color mappings for front panel
|
||||
colors.ivory = colors.pink
|
||||
colors.yellow_hc = colors.purple
|
||||
colors.red_off = colors.brown
|
||||
colors.yellow_off = colors.magenta
|
||||
colors.green_off = colors.lime
|
||||
|
||||
-- front panel styling
|
||||
|
||||
style.fp = {}
|
||||
|
||||
style.fp.root = cpair(colors.black, colors.ivory)
|
||||
style.fp.header = cpair(colors.black, colors.lightGray)
|
||||
|
||||
style.fp.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 }, -- RED ON
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xf9fb53 }, -- YELLOW ON
|
||||
{ c = colors.lime, hex = 0x16665a }, -- GREEN OFF
|
||||
{ c = colors.green, hex = 0x6be551 }, -- GREEN ON
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee }, -- YELLOW HIGH CONTRAST
|
||||
{ c = colors.pink, hex = 0xdcd9ca }, -- IVORY
|
||||
{ c = colors.magenta, hex = 0x85862c }, -- YELLOW OFF
|
||||
-- { c = colors.white, hex = 0xdcd9ca },
|
||||
{ c = colors.lightGray, hex = 0xb1b8b3 },
|
||||
{ c = colors.gray, hex = 0x575757 },
|
||||
-- { c = colors.black, hex = 0x191919 },
|
||||
{ c = colors.brown, hex = 0x672223 } -- RED OFF
|
||||
}
|
||||
|
||||
-- main GUI styling
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
style.label = cpair(colors.gray, colors.lightGray)
|
||||
@@ -33,7 +68,30 @@ style.colors = {
|
||||
-- { c = colors.brown, hex = 0x7f664c }
|
||||
}
|
||||
|
||||
-- MAIN LAYOUT --
|
||||
-- COMMON COLOR PAIRS --
|
||||
|
||||
style.wh_gray = cpair(colors.white, colors.gray)
|
||||
|
||||
style.bw_fg_bg = cpair(colors.black, colors.white)
|
||||
style.text_colors = cpair(colors.black, colors.lightGray)
|
||||
style.lu_colors = cpair(colors.gray, colors.gray)
|
||||
style.hzd_fg_bg = style.wh_gray
|
||||
style.dis_colors = cpair(colors.white, colors.lightGray)
|
||||
|
||||
style.lg_gray = cpair(colors.lightGray, colors.gray)
|
||||
style.lg_white = cpair(colors.lightGray, colors.white)
|
||||
style.gray_white = cpair(colors.gray, colors.white)
|
||||
|
||||
style.ind_grn = cpair(colors.green, colors.gray)
|
||||
style.ind_yel = cpair(colors.yellow, colors.gray)
|
||||
style.ind_red = cpair(colors.red, colors.gray)
|
||||
style.ind_wht = style.wh_gray
|
||||
|
||||
style.fp_text = cpair(colors.black, colors.ivory)
|
||||
style.fp_label = cpair(colors.lightGray, colors.ivory)
|
||||
style.led_grn = cpair(colors.green, colors.green_off)
|
||||
|
||||
-- UI COMPONENTS --
|
||||
|
||||
style.reactor = {
|
||||
-- reactor states
|
||||
@@ -151,8 +209,121 @@ style.imatrix = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "HIGH CHARGE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.sps = {
|
||||
-- SPS states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.white, colors.gray),
|
||||
text = "IDLE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ACTIVE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.dtank = {
|
||||
-- dynamic tank states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "OFF-LINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "NOT FORMED"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.orange),
|
||||
text = "RTU FAULT"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "ONLINE"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.yellow),
|
||||
text = "LOW FILL"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "FILLED"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
style.waste = {
|
||||
-- auto waste processing states
|
||||
states = {
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "PLUTONIUM"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.cyan),
|
||||
text = "POLONIUM"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.purple),
|
||||
text = "ANTI MATTER"
|
||||
}
|
||||
},
|
||||
states_abbrv = {
|
||||
{
|
||||
color = cpair(colors.black, colors.green),
|
||||
text = "Pu"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.cyan),
|
||||
text = "Po"
|
||||
},
|
||||
{
|
||||
color = cpair(colors.black, colors.purple),
|
||||
text = "AM"
|
||||
}
|
||||
},
|
||||
-- process radio button options
|
||||
options = { "Plutonium", "Polonium", "Antimatter" },
|
||||
-- unit waste selection
|
||||
unit_opts = {
|
||||
{
|
||||
text = "Auto",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.white, colors.gray)
|
||||
},
|
||||
{
|
||||
text = "Pu",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.green)
|
||||
},
|
||||
{
|
||||
text = "Po",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.cyan)
|
||||
},
|
||||
{
|
||||
text = "AM",
|
||||
fg_bg = cpair(colors.black, colors.lightGray),
|
||||
active_fg_bg = cpair(colors.black, colors.purple)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return style
|
||||
|
||||
@@ -7,17 +7,15 @@ local flasher = require("graphics.flasher")
|
||||
|
||||
local core = {}
|
||||
|
||||
core.version = "2.1.1"
|
||||
|
||||
core.flasher = flasher
|
||||
core.events = events
|
||||
|
||||
-- Core Types
|
||||
|
||||
---@enum TEXT_ALIGN
|
||||
core.TEXT_ALIGN = {
|
||||
LEFT = 1,
|
||||
CENTER = 2,
|
||||
RIGHT = 3
|
||||
}
|
||||
---@enum ALIGN
|
||||
core.ALIGN = { LEFT = 1, CENTER = 2, RIGHT = 3 }
|
||||
|
||||
---@class graphics_border
|
||||
---@field width integer
|
||||
@@ -33,11 +31,7 @@ core.TEXT_ALIGN = {
|
||||
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
|
||||
---@return graphics_border
|
||||
function core.border(width, color, even)
|
||||
return {
|
||||
width = width,
|
||||
color = color,
|
||||
even = even or false -- convert nil to false
|
||||
}
|
||||
return { width = width, color = color, even = even or false }
|
||||
end
|
||||
|
||||
---@class graphics_frame
|
||||
@@ -54,12 +48,7 @@ end
|
||||
---@param h integer
|
||||
---@return graphics_frame
|
||||
function core.gframe(x, y, w, h)
|
||||
return {
|
||||
x = x,
|
||||
y = y,
|
||||
w = w,
|
||||
h = h
|
||||
}
|
||||
return { x = x, y = y, w = w, h = h }
|
||||
end
|
||||
|
||||
---@class cpair
|
||||
@@ -80,15 +69,9 @@ end
|
||||
function core.cpair(a, b)
|
||||
return {
|
||||
-- color pairs
|
||||
color_a = a,
|
||||
color_b = b,
|
||||
blit_a = colors.toBlit(a),
|
||||
blit_b = colors.toBlit(b),
|
||||
color_a = a, color_b = b, blit_a = colors.toBlit(a), blit_b = colors.toBlit(b),
|
||||
-- aliases
|
||||
fgd = a,
|
||||
bkg = b,
|
||||
blit_fgd = colors.toBlit(a),
|
||||
blit_bkg = colors.toBlit(b)
|
||||
fgd = a, bkg = b, blit_fgd = colors.toBlit(a), blit_bkg = colors.toBlit(b)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -128,4 +111,215 @@ function core.pipe(x1, y1, x2, y2, color, thin, align_tr)
|
||||
}
|
||||
end
|
||||
|
||||
-- Assertion Handling
|
||||
|
||||
-- extract the custom element assert message, dropping the path to the element file
|
||||
function core.extract_assert_msg(msg)
|
||||
return string.sub(msg, (string.find(msg, "@") or 0) + 1)
|
||||
end
|
||||
|
||||
-- Interactive Field Manager
|
||||
|
||||
---@param e graphics_base
|
||||
---@param max_len any
|
||||
---@param fg_bg any
|
||||
---@param dis_fg_bg any
|
||||
function core.new_ifield(e, max_len, fg_bg, dis_fg_bg)
|
||||
local self = {
|
||||
frame_start = 1,
|
||||
visible_text = e.value,
|
||||
cursor_pos = string.len(e.value) + 1,
|
||||
selected_all = false
|
||||
}
|
||||
|
||||
-- update visible text
|
||||
local function _update_visible()
|
||||
self.visible_text = string.sub(e.value, self.frame_start, self.frame_start + math.min(string.len(e.value), e.frame.w) - 1)
|
||||
end
|
||||
|
||||
-- try shifting frame left
|
||||
local function _try_lshift()
|
||||
if self.frame_start > 1 then
|
||||
self.frame_start = self.frame_start - 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
-- try shifting frame right
|
||||
local function _try_rshift()
|
||||
if (self.frame_start + e.frame.w - 1) <= string.len(e.value) then
|
||||
self.frame_start = self.frame_start + 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
---@class ifield
|
||||
local public = {}
|
||||
|
||||
-- censor the display (for private info, for example) with the provided character<br>
|
||||
-- disable by passing no argument
|
||||
---@param censor string? character to hide data with
|
||||
function public.censor(censor)
|
||||
if type(censor) == "string" and string.len(censor) == 1 then
|
||||
self.censor = censor
|
||||
else self.censor = nil end
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- show the field
|
||||
function public.show()
|
||||
_update_visible()
|
||||
|
||||
if e.enabled then
|
||||
e.w_set_bkg(fg_bg.bkg)
|
||||
e.w_set_fgd(fg_bg.fgd)
|
||||
elseif dis_fg_bg ~= nil then
|
||||
e.w_set_bkg(dis_fg_bg.bkg)
|
||||
e.w_set_fgd(dis_fg_bg.fgd)
|
||||
end
|
||||
|
||||
-- clear and print
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(string.rep(" ", e.frame.w))
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
local function _write()
|
||||
if self.censor then
|
||||
e.w_write(string.rep(self.censor, string.len(self.visible_text)))
|
||||
else
|
||||
e.w_write(self.visible_text)
|
||||
end
|
||||
end
|
||||
|
||||
if e.is_focused() and e.enabled then
|
||||
-- write text with cursor
|
||||
if self.selected_all then
|
||||
e.w_set_bkg(fg_bg.fgd)
|
||||
e.w_set_fgd(fg_bg.bkg)
|
||||
_write()
|
||||
elseif self.cursor_pos >= (string.len(self.visible_text) + 1) then
|
||||
-- write text with cursor at the end, no need to blit
|
||||
_write()
|
||||
e.w_set_fgd(colors.lightGray)
|
||||
e.w_write("_")
|
||||
else
|
||||
local a, b = "", ""
|
||||
|
||||
if self.cursor_pos <= string.len(self.visible_text) then
|
||||
a = fg_bg.blit_bkg
|
||||
b = fg_bg.blit_fgd
|
||||
end
|
||||
|
||||
local b_fgd = string.rep(fg_bg.blit_fgd, self.cursor_pos - 1) .. a .. string.rep(fg_bg.blit_fgd, string.len(self.visible_text) - self.cursor_pos)
|
||||
local b_bkg = string.rep(fg_bg.blit_bkg, self.cursor_pos - 1) .. b .. string.rep(fg_bg.blit_bkg, string.len(self.visible_text) - self.cursor_pos)
|
||||
|
||||
if self.censor then
|
||||
e.w_blit(string.rep(self.censor, string.len(self.visible_text)), b_fgd, b_bkg)
|
||||
else
|
||||
e.w_blit(self.visible_text, b_fgd, b_bkg)
|
||||
end
|
||||
end
|
||||
else
|
||||
self.selected_all = false
|
||||
|
||||
-- write text without cursor
|
||||
_write()
|
||||
end
|
||||
end
|
||||
|
||||
-- move cursor to x
|
||||
---@param x integer
|
||||
function public.move_cursor(x)
|
||||
self.selected_all = false
|
||||
self.cursor_pos = math.min(x, string.len(self.visible_text) + 1)
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- select all text
|
||||
function public.select_all()
|
||||
self.selected_all = true
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- set field value
|
||||
---@param val string
|
||||
function public.set_value(val)
|
||||
e.value = string.sub(val, 1, math.min(max_len, string.len(val)))
|
||||
public.nav_end()
|
||||
end
|
||||
|
||||
-- try to insert a character if there is space
|
||||
---@param char string
|
||||
function public.try_insert_char(char)
|
||||
-- limit length
|
||||
if string.len(e.value) >= max_len then return end
|
||||
|
||||
-- replace if selected all, insert otherwise
|
||||
if self.selected_all then
|
||||
self.selected_all = false
|
||||
self.cursor_pos = 2
|
||||
self.frame_start = 1
|
||||
|
||||
e.value = char
|
||||
public.show()
|
||||
else
|
||||
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 2) .. char .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
|
||||
_update_visible()
|
||||
public.nav_right()
|
||||
end
|
||||
end
|
||||
|
||||
-- remove charcter before cursor if there is anything to remove, or delete all if selected all
|
||||
function public.backspace()
|
||||
if self.selected_all then
|
||||
self.selected_all = false
|
||||
e.value = ""
|
||||
self.cursor_pos = 1
|
||||
self.frame_start = 1
|
||||
public.show()
|
||||
else
|
||||
if self.frame_start + self.cursor_pos > 2 then
|
||||
e.value = string.sub(e.value, 1, self.frame_start + self.cursor_pos - 3) .. string.sub(e.value, self.frame_start + self.cursor_pos - 1, string.len(e.value))
|
||||
if self.cursor_pos > 1 then
|
||||
self.cursor_pos = self.cursor_pos - 1
|
||||
public.show()
|
||||
elseif _try_lshift() then public.show() end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- move cursor left by one
|
||||
function public.nav_left()
|
||||
if self.cursor_pos > 1 then
|
||||
self.cursor_pos = self.cursor_pos - 1
|
||||
public.show()
|
||||
elseif _try_lshift() then public.show() end
|
||||
end
|
||||
|
||||
-- move cursor right by one
|
||||
function public.nav_right()
|
||||
if self.cursor_pos < math.min(string.len(self.visible_text) + 1, e.frame.w) then
|
||||
self.cursor_pos = self.cursor_pos + 1
|
||||
public.show()
|
||||
elseif _try_rshift() then public.show() end
|
||||
end
|
||||
|
||||
-- move cursor to the start
|
||||
function public.nav_start()
|
||||
self.cursor_pos = 1
|
||||
self.frame_start = 1
|
||||
public.show()
|
||||
end
|
||||
|
||||
-- move cursor to the end
|
||||
function public.nav_end()
|
||||
self.frame_start = math.max(1, string.len(e.value) - e.frame.w + 2)
|
||||
_update_visible()
|
||||
self.cursor_pos = string.len(self.visible_text) + 1
|
||||
public.show()
|
||||
end
|
||||
|
||||
return public
|
||||
end
|
||||
|
||||
return core
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
-- Loading/Waiting Animation Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
@@ -8,8 +8,9 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new waiting animation element
|
||||
---@param args waiting_args
|
||||
@@ -35,49 +36,49 @@ local function waiting(args)
|
||||
|
||||
if state >= 0 and state < 7 then
|
||||
-- top
|
||||
e.window.setCursorPos(1 + math.floor(state / 2), 1)
|
||||
e.w_set_cur(1 + math.floor(state / 2), 1)
|
||||
if state % 2 == 0 then
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
end
|
||||
|
||||
-- bottom
|
||||
e.window.setCursorPos(4 - math.ceil(state / 2), 3)
|
||||
e.w_set_cur(4 - math.ceil(state / 2), 3)
|
||||
if state % 2 == 0 then
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
e.w_blit("\x8a\x85", blit_fg_2x, blit_bg_2x)
|
||||
end
|
||||
else
|
||||
local st = state - 7
|
||||
|
||||
-- right
|
||||
if st % 3 == 0 then
|
||||
e.window.setCursorPos(4, 1 + math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_bg, blit_fg)
|
||||
e.w_set_cur(4, 1 + math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_bg, blit_fg)
|
||||
elseif st % 3 == 1 then
|
||||
e.window.setCursorPos(4, 1 + math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_bg, blit_fg)
|
||||
e.window.setCursorPos(4, 2 + math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_fg, blit_bg)
|
||||
e.w_set_cur(4, 1 + math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_bg, blit_fg)
|
||||
e.w_set_cur(4, 2 + math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_fg, blit_bg)
|
||||
else
|
||||
e.window.setCursorPos(4, 2 + math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_set_cur(4, 2 + math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
end
|
||||
|
||||
-- left
|
||||
if st % 3 == 0 then
|
||||
e.window.setCursorPos(1, 3 - math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_fg, blit_bg)
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_bg, blit_fg)
|
||||
e.w_set_cur(1, 3 - math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_fg, blit_bg)
|
||||
e.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_bg, blit_fg)
|
||||
elseif st % 3 == 1 then
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x83", blit_bg, blit_fg)
|
||||
e.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x83", blit_bg, blit_fg)
|
||||
else
|
||||
e.window.setCursorPos(1, 2 - math.floor(st / 3))
|
||||
e.window.blit("\x8f", blit_fg, blit_bg)
|
||||
e.w_set_cur(1, 2 - math.floor(st / 3))
|
||||
e.w_blit("\x8f", blit_fg, blit_bg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -102,7 +103,7 @@ local function waiting(args)
|
||||
|
||||
e.start_anim()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return waiting
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
-- Color Map Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class colormap_args
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new color map
|
||||
---@param args colormap_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function colormap(args)
|
||||
local bkg = "008877FFCCEE114455DD9933BBAA2266"
|
||||
local spaces = util.spaces(32)
|
||||
local spaces = string.rep(" ", 32)
|
||||
|
||||
args.width = 32
|
||||
args.height = 1
|
||||
@@ -24,10 +23,15 @@ local function colormap(args)
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw color map
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(spaces, bkg, bkg)
|
||||
function e.redraw()
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(spaces, bkg, bkg)
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return colormap
|
||||
|
||||
132
graphics/elements/controls/app.lua
Normal file
132
graphics/elements/controls/app.lua
Normal file
@@ -0,0 +1,132 @@
|
||||
-- App Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class app_button_args
|
||||
---@field text string app icon text
|
||||
---@field title string app title text
|
||||
---@field callback function function to call on touch
|
||||
---@field app_fg_bg cpair app icon foreground/background colors
|
||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new app button
|
||||
---@param args app_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function app_button(args)
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
element.assert(type(args.title) == "string", "title is a required field")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
|
||||
|
||||
args.height = 4
|
||||
args.width = 5
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw the app button
|
||||
local function draw()
|
||||
local fgd = args.app_fg_bg.fgd
|
||||
local bkg = args.app_fg_bg.bkg
|
||||
|
||||
if e.value then
|
||||
fgd = args.active_fg_bg.fgd
|
||||
bkg = args.active_fg_bg.bkg
|
||||
end
|
||||
|
||||
-- draw icon
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_write("\x9f\x83\x83\x83")
|
||||
e.w_set_fgd(bkg)
|
||||
e.w_set_bkg(fgd)
|
||||
e.w_write("\x90")
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_set_cur(1, 2)
|
||||
e.w_write("\x95 ")
|
||||
e.w_set_fgd(bkg)
|
||||
e.w_set_bkg(fgd)
|
||||
e.w_write("\x95")
|
||||
e.w_set_cur(1, 3)
|
||||
e.w_write("\x82\x8f\x8f\x8f\x81")
|
||||
|
||||
-- write the icon text
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_set_fgd(fgd)
|
||||
e.w_set_bkg(bkg)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- draw the app button as pressed (if active_fg_bg set)
|
||||
local function show_pressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = true
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- draw the app button as unpressed (if active_fg_bg set)
|
||||
local function show_unpressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
draw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
show_pressed()
|
||||
-- show as unpressed in 0.25 seconds
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||
args.callback()
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
show_pressed()
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
show_unpressed()
|
||||
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the app button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
e.w_set_cur(math.floor((e.frame.w - string.len(args.title)) / 2) + 1, 4)
|
||||
e.w_write(args.title)
|
||||
draw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return app_button
|
||||
121
graphics/elements/controls/checkbox.lua
Normal file
121
graphics/elements/controls/checkbox.lua
Normal file
@@ -0,0 +1,121 @@
|
||||
-- Checkbox Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class checkbox_args
|
||||
---@field label string checkbox text
|
||||
---@field box_fg_bg cpair colors for checkbox
|
||||
---@field default? boolean default value
|
||||
---@field callback? function function to call on press
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new checkbox control
|
||||
---@param args checkbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function checkbox(args)
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.box_fg_bg) == "table", "box_fg_bg is a required field")
|
||||
|
||||
args.can_focus = true
|
||||
args.height = 1
|
||||
args.width = 2 + string.len(args.label)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = args.default == true
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if e.value then
|
||||
-- show as selected
|
||||
e.w_set_fgd(args.box_fg_bg.bkg)
|
||||
e.w_set_bkg(args.box_fg_bg.fgd)
|
||||
e.w_write("\x88")
|
||||
e.w_set_fgd(args.box_fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
else
|
||||
-- show as unselected
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(args.box_fg_bg.bkg)
|
||||
e.w_write("\x88")
|
||||
e.w_set_fgd(args.box_fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
end
|
||||
end
|
||||
|
||||
-- write label text
|
||||
local function draw_label()
|
||||
if e.enabled and e.is_focused() then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
e.w_write(args.label)
|
||||
else
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
e.value = not e.value
|
||||
draw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == core.events.KEY_CLICK.DOWN then
|
||||
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
|
||||
e.value = not e.value
|
||||
draw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
end
|
||||
|
||||
-- handle focus
|
||||
e.on_focused = draw_label
|
||||
e.on_unfocused = draw_label
|
||||
|
||||
-- handle enable
|
||||
e.on_enabled = draw_label
|
||||
e.on_disabled = draw_label
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
draw()
|
||||
draw_label()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return checkbox
|
||||
@@ -1,7 +1,6 @@
|
||||
-- Hazard-bordered Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local util = require("scada-common.util")
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
@@ -14,54 +13,50 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new hazard button
|
||||
---@param args hazard_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function hazard_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.hazard_button: text is a required field")
|
||||
assert(type(args.accent) == "number", "graphics.elements.controls.hazard_button: accent is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.hazard_button: callback is a required field")
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
element.assert(type(args.accent) == "number", "accent is a required field")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
|
||||
-- static dimensions
|
||||
args.height = 3
|
||||
args.width = string.len(args.text) + 4
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
|
||||
-- draw border
|
||||
---@param accent color accent color
|
||||
local function draw_border(accent)
|
||||
-- top
|
||||
e.window.setTextColor(accent)
|
||||
e.window.setBackgroundColor(args.fg_bg.bkg)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write("\x99" .. util.strrep("\x89", args.width - 2) .. "\x99")
|
||||
e.w_set_fgd(accent)
|
||||
e.w_set_bkg(args.fg_bg.bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write("\x99" .. string.rep("\x89", args.width - 2) .. "\x99")
|
||||
|
||||
-- center left
|
||||
e.window.setCursorPos(1, 2)
|
||||
e.window.setTextColor(args.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(accent)
|
||||
e.window.write("\x99")
|
||||
e.w_set_cur(1, 2)
|
||||
e.w_set_fgd(args.fg_bg.bkg)
|
||||
e.w_set_bkg(accent)
|
||||
e.w_write("\x99")
|
||||
|
||||
-- center right
|
||||
e.window.setTextColor(args.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(accent)
|
||||
e.window.setCursorPos(args.width, 2)
|
||||
e.window.write("\x99")
|
||||
e.w_set_fgd(args.fg_bg.bkg)
|
||||
e.w_set_bkg(accent)
|
||||
e.w_set_cur(args.width, 2)
|
||||
e.w_write("\x99")
|
||||
|
||||
-- bottom
|
||||
e.window.setTextColor(accent)
|
||||
e.window.setBackgroundColor(args.fg_bg.bkg)
|
||||
e.window.setCursorPos(1, 3)
|
||||
e.window.write("\x99" .. util.strrep("\x98", args.width - 2) .. "\x99")
|
||||
e.w_set_fgd(accent)
|
||||
e.w_set_bkg(args.fg_bg.bkg)
|
||||
e.w_set_cur(1, 3)
|
||||
e.w_write("\x99" .. string.rep("\x98", args.width - 2) .. "\x99")
|
||||
end
|
||||
|
||||
-- on request timeout: recursively calls itself to double flash button text
|
||||
@@ -72,9 +67,9 @@ local function hazard_button(args)
|
||||
|
||||
if n == 0 then
|
||||
-- go back off
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
if n >= 4 then
|
||||
@@ -82,18 +77,18 @@ local function hazard_button(args)
|
||||
elseif n % 2 == 0 then
|
||||
-- toggle text color on after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_timeout(n + 1)
|
||||
on_timeout(n + 1)
|
||||
end)
|
||||
elseif n % 1 then
|
||||
-- toggle text color off after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_timeout(n + 1)
|
||||
end)
|
||||
end
|
||||
@@ -101,9 +96,9 @@ local function hazard_button(args)
|
||||
|
||||
-- blink routine for success indication
|
||||
local function on_success()
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- blink routine for failure indication
|
||||
@@ -114,9 +109,9 @@ local function hazard_button(args)
|
||||
|
||||
if n == 0 then
|
||||
-- go back off
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
if n >= 2 then
|
||||
@@ -124,17 +119,17 @@ local function hazard_button(args)
|
||||
elseif n % 2 == 0 then
|
||||
-- toggle text color on after 0.5 seconds
|
||||
tcd.dispatch(0.5, function ()
|
||||
e.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_failure(n + 1)
|
||||
end)
|
||||
elseif n % 1 then
|
||||
-- toggle text color off after 0.25 seconds
|
||||
tcd.dispatch(0.25, function ()
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
on_failure(n + 1)
|
||||
end)
|
||||
end
|
||||
@@ -146,9 +141,9 @@ local function hazard_button(args)
|
||||
if e.enabled then
|
||||
if core.events.was_clicked(event.type) then
|
||||
-- change text color to indicate clicked
|
||||
e.window.setTextColor(args.accent)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.accent)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
|
||||
-- abort any other callbacks
|
||||
tcd.abort(on_timeout)
|
||||
@@ -158,7 +153,6 @@ local function hazard_button(args)
|
||||
-- 1.5 second timeout
|
||||
tcd.dispatch(1.5, on_timeout)
|
||||
|
||||
-- call the touch callback
|
||||
args.callback()
|
||||
end
|
||||
end
|
||||
@@ -174,31 +168,39 @@ local function hazard_button(args)
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- show the button as disabled
|
||||
function e.disable()
|
||||
function e.on_disabled()
|
||||
if args.dis_colors then
|
||||
draw_border(args.dis_colors.color_a)
|
||||
e.window.setTextColor(args.dis_colors.color_b)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.dis_colors.color_b)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
end
|
||||
|
||||
-- show the button as enabled
|
||||
function e.enable()
|
||||
function e.on_enabled()
|
||||
draw_border(args.accent)
|
||||
e.window.setTextColor(args.fg_bg.fgd)
|
||||
e.window.setCursorPos(3, 2)
|
||||
e.window.write(args.text)
|
||||
e.w_set_fgd(args.fg_bg.fgd)
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- initial draw of border
|
||||
draw_border(args.accent)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
-- write the button text and draw border
|
||||
e.w_set_cur(3, 2)
|
||||
e.w_write(args.text)
|
||||
draw_border(args.accent)
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return hazard_button
|
||||
|
||||
@@ -20,21 +20,20 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new multi button (latch selection, exclusively one button at a time)
|
||||
---@param args multi_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multi_button(args)
|
||||
assert(type(args.options) == "table", "graphics.elements.controls.multi_button: options is a required field")
|
||||
assert(#args.options > 0, "graphics.elements.controls.multi_button: at least one option is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.multi_button: callback is a required field")
|
||||
assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0),
|
||||
"graphics.elements.controls.multi_button: default must be nil or a number > 0")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.multi_button: min_width must be nil or a number > 0")
|
||||
element.assert(type(args.options) == "table", "options is a required field")
|
||||
element.assert(#args.options > 0, "at least one option is required")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
@@ -70,23 +69,23 @@ local function multi_button(args)
|
||||
end
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
function e.redraw()
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type button_option
|
||||
|
||||
e.window.setCursorPos(opt._start_x, 1)
|
||||
e.w_set_cur(opt._start_x, 1)
|
||||
|
||||
if e.value == i then
|
||||
-- show as pressed
|
||||
e.window.setTextColor(opt.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(opt.active_fg_bg.bkg)
|
||||
e.w_set_fgd(opt.active_fg_bg.fgd)
|
||||
e.w_set_bkg(opt.active_fg_bg.bkg)
|
||||
else
|
||||
-- show as unpressed
|
||||
e.window.setTextColor(opt.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(opt.fg_bg.bkg)
|
||||
e.w_set_fgd(opt.fg_bg.fgd)
|
||||
e.w_set_bkg(opt.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.window.write(util.pad(opt.text, button_width))
|
||||
e.w_write(util.pad(opt.text, button_width))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -114,7 +113,7 @@ local function multi_button(args)
|
||||
-- tap always has identical coordinates, so this always passes for taps
|
||||
if button_ini == button_cur and button_cur ~= nil then
|
||||
e.value = button_cur
|
||||
draw()
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
@@ -124,13 +123,13 @@ local function multi_button(args)
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
e.redraw()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return multi_button
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
-- Button Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
|
||||
---@class push_button_args
|
||||
---@field text string button text
|
||||
---@field callback function function to call on touch
|
||||
---@field min_width? integer text length if omitted
|
||||
---@field alignment? ALIGN text align if min width > length
|
||||
---@field active_fg_bg? cpair foreground/background colors when pressed
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new push button
|
||||
---@param args push_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function push_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.push_button: text is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.push_button: callback is a required field")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.push_button: min_width must be nil or a number > 0")
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
|
||||
local text_width = string.len(args.text)
|
||||
local alignment = args.alignment or ALIGN.CENTER
|
||||
|
||||
-- single line height, calculate width
|
||||
-- set automatic settings
|
||||
args.can_focus = true
|
||||
args.height = 1
|
||||
args.min_width = args.min_width or 0
|
||||
args.width = math.max(text_width, args.min_width)
|
||||
@@ -39,25 +45,31 @@ local function push_button(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
local h_pad = 1
|
||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||
|
||||
if alignment == ALIGN.CENTER then
|
||||
h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
elseif alignment == ALIGN.RIGHT then
|
||||
h_pad = (e.frame.w - text_width) + 1
|
||||
end
|
||||
|
||||
-- draw the button
|
||||
local function draw()
|
||||
function e.redraw()
|
||||
e.window.clear()
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(h_pad, v_pad)
|
||||
e.window.write(args.text)
|
||||
e.w_set_cur(h_pad, v_pad)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- draw the button as pressed (if active_fg_bg set)
|
||||
local function show_pressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = true
|
||||
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||
draw()
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -65,9 +77,9 @@ local function push_button(args)
|
||||
local function show_unpressed()
|
||||
if e.enabled and args.active_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
draw()
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -75,14 +87,14 @@ local function push_button(args)
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == CLICK_TYPE.TAP then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
show_pressed()
|
||||
-- show as unpressed in 0.25 seconds
|
||||
if args.active_fg_bg ~= nil then tcd.dispatch(0.25, show_unpressed) end
|
||||
args.callback()
|
||||
elseif event.type == CLICK_TYPE.DOWN then
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
show_pressed()
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
show_unpressed()
|
||||
if e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
args.callback()
|
||||
@@ -91,36 +103,51 @@ local function push_button(args)
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.DOWN then
|
||||
if event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter then
|
||||
args.callback()
|
||||
e.defocus()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (true simulates pressing the button)
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.CLICK_TYPE.UP, 1, 1)) end
|
||||
if val then e.handle_mouse(core.events.mouse_generic(core.events.MOUSE_CLICK.UP, 1, 1)) end
|
||||
end
|
||||
|
||||
-- show butten as enabled
|
||||
function e.enable()
|
||||
function e.on_enabled()
|
||||
if args.dis_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
draw()
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- show button as disabled
|
||||
function e.disable()
|
||||
function e.on_disabled()
|
||||
if args.dis_fg_bg ~= nil then
|
||||
e.value = false
|
||||
e.window.setTextColor(args.dis_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.dis_fg_bg.bkg)
|
||||
draw()
|
||||
e.w_set_fgd(args.dis_fg_bg.fgd)
|
||||
e.w_set_bkg(args.dis_fg_bg.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
-- handle focus
|
||||
e.on_focused = show_pressed
|
||||
e.on_unfocused = show_unpressed
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return push_button
|
||||
|
||||
203
graphics/elements/controls/radio_2d.lua
Normal file
203
graphics/elements/controls/radio_2d.lua
Normal file
@@ -0,0 +1,203 @@
|
||||
-- 2D Radio Button Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class radio_2d_args
|
||||
---@field rows integer
|
||||
---@field columns integer
|
||||
---@field options table
|
||||
---@field radio_colors cpair radio button colors (inner & outer)
|
||||
---@field select_color? color color for radio button when selected
|
||||
---@field color_map? table colors for each radio button when selected
|
||||
---@field disable_color? color color for radio button when disabled
|
||||
---@field disable_fg_bg? cpair text colors when disabled
|
||||
---@field default? integer default state, defaults to options[1]
|
||||
---@field callback? function function to call on touch
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new 2D radio button list (latch selection, exclusively one color at a time)
|
||||
---@param args radio_2d_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function radio_2d_button(args)
|
||||
element.assert(type(args.options) == "table" and #args.options > 0, "options should be a table with length >= 1")
|
||||
element.assert(util.is_int(args.rows) and util.is_int(args.columns), "rows/columns must be integers")
|
||||
element.assert((args.rows * args.columns) >= #args.options, "rows x columns size insufficient for provided number of options")
|
||||
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
|
||||
element.assert(type(args.select_color) == "number" or type(args.color_map) == "table", "select_color or color_map is required")
|
||||
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
|
||||
|
||||
local array = {}
|
||||
local col_widths = {}
|
||||
|
||||
local next_idx = 1
|
||||
local total_width = 0
|
||||
local max_rows = 1
|
||||
|
||||
local focused_opt = 1
|
||||
local focus_x, focus_y = 1, 1
|
||||
|
||||
-- build table to display
|
||||
for col = 1, args.columns do
|
||||
local max_width = 0
|
||||
array[col] = {}
|
||||
|
||||
for row = 1, args.rows do
|
||||
local len = string.len(args.options[next_idx])
|
||||
if len > max_width then max_width = len end
|
||||
if row > max_rows then max_rows = row end
|
||||
|
||||
table.insert(array[col], { text = args.options[next_idx], id = next_idx, x_1 = 1 + total_width, x_2 = 2 + total_width + len })
|
||||
|
||||
next_idx = next_idx + 1
|
||||
if next_idx > #args.options then break end
|
||||
end
|
||||
|
||||
table.insert(col_widths, max_width + 3)
|
||||
total_width = total_width + max_width + 3
|
||||
if next_idx > #args.options then break end
|
||||
end
|
||||
|
||||
args.can_focus = true
|
||||
args.width = total_width
|
||||
args.height = max_rows
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- selected option (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
|
||||
-- draw the element
|
||||
function e.redraw()
|
||||
local col_x = 1
|
||||
|
||||
local radio_color_b = util.trinary(type(args.disable_color) == "number" and not e.enabled, args.disable_color, args.radio_colors.color_b)
|
||||
|
||||
for col = 1, #array do
|
||||
for row = 1, #array[col] do
|
||||
local opt = array[col][row]
|
||||
local select_color = args.select_color
|
||||
|
||||
if type(args.color_map) == "table" and args.color_map[opt.id] then
|
||||
select_color = args.color_map[opt.id]
|
||||
end
|
||||
|
||||
local inner_color = util.trinary((e.value == opt.id) and e.enabled, radio_color_b, args.radio_colors.color_a)
|
||||
local outer_color = util.trinary((e.value == opt.id) and e.enabled, select_color, radio_color_b)
|
||||
|
||||
e.w_set_cur(col_x, row)
|
||||
|
||||
e.w_set_fgd(inner_color)
|
||||
e.w_set_bkg(outer_color)
|
||||
e.w_write("\x88")
|
||||
|
||||
e.w_set_fgd(outer_color)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
|
||||
if opt.id == focused_opt then
|
||||
focus_x, focus_y = row, col
|
||||
end
|
||||
|
||||
-- write button text
|
||||
if opt.id == focused_opt and e.is_focused() and e.enabled then
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
elseif type(args.disable_fg_bg) == "table" and not e.enabled then
|
||||
e.w_set_fgd(args.disable_fg_bg.fgd)
|
||||
e.w_set_bkg(args.disable_fg_bg.bkg)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.w_write(opt.text)
|
||||
end
|
||||
|
||||
col_x = col_x + col_widths[col]
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) and (event.initial.y == event.current.y) then
|
||||
-- determine what was pressed
|
||||
for _, row in ipairs(array) do
|
||||
local elem = row[event.current.y]
|
||||
if elem ~= nil and event.initial.x >= elem.x_1 and event.initial.x <= elem.x_2 and event.current.x >= elem.x_1 and event.current.x <= elem.x_2 then
|
||||
e.value = elem.id
|
||||
focused_opt = elem.id
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == core.events.KEY_CLICK.DOWN or event.type == core.events.KEY_CLICK.HELD then
|
||||
if event.type == core.events.KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
|
||||
e.value = focused_opt
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
elseif event.key == keys.down then
|
||||
if focused_opt < #args.options then
|
||||
focused_opt = focused_opt + 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.up then
|
||||
if focused_opt > 1 then
|
||||
focused_opt = focused_opt - 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.right then
|
||||
if array[focus_y + 1] and array[focus_y + 1][focus_x] then
|
||||
focused_opt = array[focus_y + 1][focus_x].id
|
||||
else focused_opt = array[1][focus_x].id end
|
||||
e.redraw()
|
||||
elseif event.key == keys.left then
|
||||
if array[focus_y - 1] and array[focus_y - 1][focus_x] then
|
||||
focused_opt = array[focus_y - 1][focus_x].id
|
||||
e.redraw()
|
||||
elseif array[#array][focus_x] then
|
||||
focused_opt = array[#array][focus_x].id
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
if type(val) == "number" and val > 0 and val <= #args.options then
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle focus & enable
|
||||
e.on_focused = e.redraw
|
||||
e.on_unfocused = e.redraw
|
||||
e.on_enabled = e.redraw
|
||||
e.on_disabled = e.redraw
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return radio_2d_button
|
||||
@@ -1,35 +1,36 @@
|
||||
-- Radio Button Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
|
||||
---@class radio_button_args
|
||||
---@field options table button options
|
||||
---@field callback function function to call on touch
|
||||
---@field radio_colors cpair colors for radio button center dot when active (a) or inactive (b)
|
||||
---@field radio_bg color background color of radio button
|
||||
---@field radio_colors cpair radio button colors (inner & outer)
|
||||
---@field select_color color color for radio button border when selected
|
||||
---@field default? integer default state, defaults to options[1]
|
||||
---@field min_width? integer text length + 2 if omitted
|
||||
---@field callback? function function to call on touch
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new radio button list (latch selection, exclusively one button at a time)
|
||||
---@param args radio_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function radio_button(args)
|
||||
assert(type(args.options) == "table", "graphics.elements.controls.radio_button: options is a required field")
|
||||
assert(#args.options > 0, "graphics.elements.controls.radio_button: at least one option is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.radio_button: callback is a required field")
|
||||
assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0),
|
||||
"graphics.elements.controls.radio_button: default must be nil or a number > 0")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.radio_button: min_width must be nil or a number > 0")
|
||||
|
||||
-- one line per option
|
||||
args.height = #args.options
|
||||
element.assert(type(args.options) == "table", "options is a required field")
|
||||
element.assert(#args.options > 0, "at least one option is required")
|
||||
element.assert(type(args.radio_colors) == "table", "radio_colors is a required field")
|
||||
element.assert(type(args.select_color) == "number", "select_color is a required field")
|
||||
element.assert(type(args.default) == "nil" or (type(args.default) == "number" and args.default > 0), "default must be nil or a number > 0")
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
|
||||
-- determine widths
|
||||
local max_width = 1
|
||||
@@ -42,41 +43,47 @@ local function radio_button(args)
|
||||
|
||||
local button_text_width = math.max(max_width, args.min_width or 0)
|
||||
|
||||
-- set automatic args
|
||||
args.can_focus = true
|
||||
args.width = button_text_width + 2
|
||||
args.height = #args.options -- one line per option
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local focused_opt = 1
|
||||
|
||||
-- button state (convert nil to 1 if missing)
|
||||
e.value = args.default or 1
|
||||
|
||||
-- show the button state
|
||||
local function draw()
|
||||
function e.redraw()
|
||||
for i = 1, #args.options do
|
||||
local opt = args.options[i] ---@type string
|
||||
|
||||
e.window.setCursorPos(1, i)
|
||||
local inner_color = util.trinary(e.value == i, args.radio_colors.color_b, args.radio_colors.color_a)
|
||||
local outer_color = util.trinary(e.value == i, args.select_color, args.radio_colors.color_b)
|
||||
|
||||
if e.value == i then
|
||||
-- show as selected
|
||||
e.window.setTextColor(args.radio_colors.color_a)
|
||||
e.window.setBackgroundColor(args.radio_bg)
|
||||
else
|
||||
-- show as unselected
|
||||
e.window.setTextColor(args.radio_colors.color_b)
|
||||
e.window.setBackgroundColor(args.radio_bg)
|
||||
end
|
||||
e.w_set_cur(1, i)
|
||||
|
||||
e.window.write("\x88")
|
||||
e.w_set_fgd(inner_color)
|
||||
e.w_set_bkg(outer_color)
|
||||
e.w_write("\x88")
|
||||
|
||||
e.window.setTextColor(args.radio_bg)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write("\x95")
|
||||
e.w_set_fgd(outer_color)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_write("\x95")
|
||||
|
||||
-- write button text
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.write(opt)
|
||||
if i == focused_opt and e.is_focused() and e.enabled then
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
else
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.w_write(opt)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -87,8 +94,31 @@ local function radio_button(args)
|
||||
-- determine what was pressed
|
||||
if args.options[event.current.y] ~= nil then
|
||||
e.value = event.current.y
|
||||
draw()
|
||||
args.callback(e.value)
|
||||
focused_opt = e.value
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if event.type == KEY_CLICK.DOWN and (event.key == keys.space or event.key == keys.enter or event.key == keys.numPadEnter) then
|
||||
e.value = focused_opt
|
||||
e.redraw()
|
||||
if type(args.callback) == "function" then args.callback(e.value) end
|
||||
elseif event.key == keys.down then
|
||||
if focused_opt < #args.options then
|
||||
focused_opt = focused_opt + 1
|
||||
e.redraw()
|
||||
end
|
||||
elseif event.key == keys.up then
|
||||
if focused_opt > 1 then
|
||||
focused_opt = focused_opt - 1
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -96,14 +126,22 @@ local function radio_button(args)
|
||||
-- set the value
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
if type(val) == "number" and val > 0 and val <= #args.options then
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
-- handle focus & enable
|
||||
e.on_focused = e.redraw
|
||||
e.on_unfocused = e.redraw
|
||||
e.on_enabled = e.redraw
|
||||
e.on_disabled = e.redraw
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return radio_button
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
-- Sidebar Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class sidebar_tab
|
||||
---@field char string character identifier
|
||||
@@ -17,33 +18,37 @@ local CLICK_TYPE = core.events.CLICK_TYPE
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new sidebar tab selector
|
||||
---@param args sidebar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function sidebar(args)
|
||||
assert(type(args.tabs) == "table", "graphics.elements.controls.sidebar: tabs is a required field")
|
||||
assert(#args.tabs > 0, "graphics.elements.controls.sidebar: at least one tab is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.sidebar: callback is a required field")
|
||||
element.assert(type(args.tabs) == "table", "tabs is a required field")
|
||||
element.assert(#args.tabs > 0, "at least one tab is required")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
|
||||
-- always 3 wide
|
||||
args.width = 3
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
assert(e.frame.h >= (#args.tabs * 3), "graphics.elements.controls.sidebar: height insufficent to display all tabs")
|
||||
element.assert(e.frame.h >= (#args.tabs * 3), "height insufficent to display all tabs")
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
|
||||
local was_pressed = false
|
||||
|
||||
-- show the button state
|
||||
---@param pressed boolean if the currently selected tab should appear as actively pressed
|
||||
---@param pressed? boolean if the currently selected tab should appear as actively pressed
|
||||
---@param pressed_idx? integer optional index to show as held (that is not yet selected)
|
||||
local function draw(pressed, pressed_idx)
|
||||
pressed = util.trinary(pressed == nil, was_pressed, pressed)
|
||||
was_pressed = pressed
|
||||
pressed_idx = pressed_idx or e.value
|
||||
|
||||
for i = 1, #args.tabs do
|
||||
@@ -51,27 +56,23 @@ local function sidebar(args)
|
||||
|
||||
local y = ((i - 1) * 3) + 1
|
||||
|
||||
e.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
|
||||
if pressed and i == pressed_idx then
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
else
|
||||
e.window.setTextColor(tab.color.fgd)
|
||||
e.window.setBackgroundColor(tab.color.bkg)
|
||||
e.w_set_fgd(tab.color.fgd)
|
||||
e.w_set_bkg(tab.color.bkg)
|
||||
end
|
||||
|
||||
e.window.write(" ")
|
||||
e.window.setCursorPos(1, y + 1)
|
||||
e.w_write(" ")
|
||||
e.w_set_cur(1, y + 1)
|
||||
if e.value == i then
|
||||
-- show as selected
|
||||
e.window.write(" " .. tab.char .. "\x10")
|
||||
else
|
||||
-- show as unselected
|
||||
e.window.write(" " .. tab.char .. " ")
|
||||
end
|
||||
e.window.setCursorPos(1, y + 2)
|
||||
e.window.write(" ")
|
||||
e.w_write(" " .. tab.char .. "\x10")
|
||||
else e.w_write(" " .. tab.char .. " ") end
|
||||
e.w_set_cur(1, y + 2)
|
||||
e.w_write(" ")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,22 +85,22 @@ local function sidebar(args)
|
||||
local ini_idx = math.ceil(event.initial.y / 3)
|
||||
|
||||
if args.tabs[cur_idx] ~= nil then
|
||||
if event.type == CLICK_TYPE.TAP then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
e.value = cur_idx
|
||||
draw(true)
|
||||
-- show as unpressed in 0.25 seconds
|
||||
tcd.dispatch(0.25, function () draw(false) end)
|
||||
args.callback(e.value)
|
||||
elseif event.type == CLICK_TYPE.DOWN then
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
draw(true, cur_idx)
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
e.value = cur_idx
|
||||
draw(false)
|
||||
args.callback(e.value)
|
||||
else draw(false) end
|
||||
end
|
||||
elseif event.type == CLICK_TYPE.UP then
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
draw(false)
|
||||
end
|
||||
end
|
||||
@@ -112,10 +113,12 @@ local function sidebar(args)
|
||||
draw(false)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw(false)
|
||||
-- element redraw
|
||||
e.redraw = draw
|
||||
|
||||
return e.get()
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return sidebar
|
||||
|
||||
@@ -16,8 +16,9 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new spinbox control (minimum value is 0)
|
||||
---@param args spinbox_args
|
||||
@@ -28,8 +29,8 @@ local function spinbox(args)
|
||||
local wn_prec = args.whole_num_precision
|
||||
local fr_prec = args.fractional_precision
|
||||
|
||||
assert(util.is_int(wn_prec), "graphics.element.controls.spinbox_numeric: whole number precision must be an integer")
|
||||
assert(util.is_int(fr_prec), "graphics.element.controls.spinbox_numeric: fractional precision must be an integer")
|
||||
element.assert(util.is_int(wn_prec), "whole number precision must be an integer")
|
||||
element.assert(util.is_int(fr_prec), "fractional precision must be an integer")
|
||||
|
||||
local fmt, fmt_init ---@type string, string
|
||||
|
||||
@@ -43,7 +44,7 @@ local function spinbox(args)
|
||||
|
||||
local dec_point_x = args.whole_num_precision + 1
|
||||
|
||||
assert(type(args.arrow_fg_bg) == "table", "graphics.element.spinbox_numeric: arrow_fg_bg is a required field")
|
||||
element.assert(type(args.arrow_fg_bg) == "table", "arrow_fg_bg is a required field")
|
||||
|
||||
-- determine widths
|
||||
args.width = wn_prec + fr_prec + util.trinary(fr_prec > 0, 1, 0)
|
||||
@@ -57,22 +58,20 @@ local function spinbox(args)
|
||||
|
||||
-- draw the arrows
|
||||
local function draw_arrows(color)
|
||||
e.window.setBackgroundColor(args.arrow_fg_bg.bkg)
|
||||
e.window.setTextColor(color)
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(util.strrep("\x1e", wn_prec))
|
||||
e.window.setCursorPos(1, 3)
|
||||
e.window.write(util.strrep("\x1f", wn_prec))
|
||||
e.w_set_bkg(args.arrow_fg_bg.bkg)
|
||||
e.w_set_fgd(color)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(string.rep("\x1e", wn_prec))
|
||||
e.w_set_cur(1, 3)
|
||||
e.w_write(string.rep("\x1f", wn_prec))
|
||||
if fr_prec > 0 then
|
||||
e.window.setCursorPos(1 + wn_prec, 1)
|
||||
e.window.write(" " .. util.strrep("\x1e", fr_prec))
|
||||
e.window.setCursorPos(1 + wn_prec, 3)
|
||||
e.window.write(" " .. util.strrep("\x1f", fr_prec))
|
||||
e.w_set_cur(1 + wn_prec, 1)
|
||||
e.w_write(" " .. string.rep("\x1e", fr_prec))
|
||||
e.w_set_cur(1 + wn_prec, 3)
|
||||
e.w_write(" " .. string.rep("\x1f", fr_prec))
|
||||
end
|
||||
end
|
||||
|
||||
draw_arrows(args.arrow_fg_bg.fgd)
|
||||
|
||||
-- populate digits from current value
|
||||
local function set_digits()
|
||||
local initial_str = util.sprintf(fmt_init, e.value)
|
||||
@@ -118,15 +117,12 @@ local function spinbox(args)
|
||||
end
|
||||
|
||||
-- draw
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setCursorPos(1, 2)
|
||||
e.window.write(util.sprintf(fmt, e.value))
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_cur(1, 2)
|
||||
e.w_write(util.sprintf(fmt, e.value))
|
||||
end
|
||||
|
||||
-- init with the default value
|
||||
show_num()
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
@@ -137,10 +133,8 @@ local function spinbox(args)
|
||||
local idx = util.trinary(event.current.x > dec_point_x, event.current.x - 1, event.current.x)
|
||||
if digits[idx] ~= nil then
|
||||
if event.current.y == 1 then
|
||||
-- increment
|
||||
digits[idx] = digits[idx] + 1
|
||||
elseif event.current.y == 3 then
|
||||
-- decrement
|
||||
digits[idx] = digits[idx] - 1
|
||||
end
|
||||
|
||||
@@ -175,20 +169,21 @@ local function spinbox(args)
|
||||
end
|
||||
|
||||
-- enable this input
|
||||
function e.enable()
|
||||
draw_arrows(args.arrow_fg_bg.fgd)
|
||||
end
|
||||
function e.on_enabled() draw_arrows(args.arrow_fg_bg.fgd) end
|
||||
|
||||
-- disable this input
|
||||
function e.disable()
|
||||
draw_arrows(args.arrow_disable or colors.lightGray)
|
||||
function e.on_disabled() draw_arrows(args.arrow_disable or colors.lightGray) end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
show_num()
|
||||
draw_arrows(util.trinary(e.enabled, args.arrow_fg_bg.fgd, args.arrow_disable or colors.lightGray))
|
||||
end
|
||||
|
||||
-- default to zero, init digits table
|
||||
e.value = 0
|
||||
set_digits()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return spinbox
|
||||
|
||||
@@ -12,23 +12,22 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new switch button (latch high/low)
|
||||
---@param args switch_button_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function switch_button(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.controls.switch_button: text is a required field")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.switch_button: callback is a required field")
|
||||
assert(type(args.active_fg_bg) == "table", "graphics.elements.controls.switch_button: active_fg_bg is a required field")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.switch_button: min_width must be nil or a number > 0")
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
element.assert(type(args.active_fg_bg) == "table", "active_fg_bg is a required field")
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
|
||||
local text_width = string.len(args.text)
|
||||
|
||||
-- single line height, calculate width
|
||||
args.height = 1
|
||||
args.min_width = args.min_width or 0
|
||||
args.width = math.max(text_width, args.min_width)
|
||||
@@ -36,44 +35,32 @@ local function switch_button(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- button state (convert nil to false if missing)
|
||||
e.value = args.default or false
|
||||
|
||||
local h_pad = math.floor((e.frame.w - text_width) / 2) + 1
|
||||
local v_pad = math.floor(e.frame.h / 2) + 1
|
||||
|
||||
-- show the button state
|
||||
local function draw_state()
|
||||
function e.redraw()
|
||||
if e.value then
|
||||
-- show as pressed
|
||||
e.window.setTextColor(args.active_fg_bg.fgd)
|
||||
e.window.setBackgroundColor(args.active_fg_bg.bkg)
|
||||
e.w_set_fgd(args.active_fg_bg.fgd)
|
||||
e.w_set_bkg(args.active_fg_bg.bkg)
|
||||
else
|
||||
-- show as unpressed
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- clear to redraw background
|
||||
e.window.clear()
|
||||
|
||||
-- write the button text
|
||||
e.window.setCursorPos(h_pad, v_pad)
|
||||
e.window.write(args.text)
|
||||
e.w_set_cur(h_pad, v_pad)
|
||||
e.w_write(args.text)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw_state()
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled and core.events.was_clicked(event.type) then
|
||||
-- toggle state
|
||||
e.value = not e.value
|
||||
draw_state()
|
||||
|
||||
-- call the touch callback with state
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
@@ -81,12 +68,14 @@ local function switch_button(args)
|
||||
-- set the value
|
||||
---@param val boolean new value
|
||||
function e.set_value(val)
|
||||
-- set state
|
||||
e.value = val
|
||||
draw_state()
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return switch_button
|
||||
|
||||
@@ -18,21 +18,20 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tab selector
|
||||
---@param args tabbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tabbar(args)
|
||||
assert(type(args.tabs) == "table", "graphics.elements.controls.tabbar: tabs is a required field")
|
||||
assert(#args.tabs > 0, "graphics.elements.controls.tabbar: at least one tab is required")
|
||||
assert(type(args.callback) == "function", "graphics.elements.controls.tabbar: callback is a required field")
|
||||
assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0),
|
||||
"graphics.elements.controls.tabbar: min_width must be nil or a number > 0")
|
||||
element.assert(type(args.tabs) == "table", "tabs is a required field")
|
||||
element.assert(#args.tabs > 0, "at least one tab is required")
|
||||
element.assert(type(args.callback) == "function", "callback is a required field")
|
||||
element.assert(type(args.min_width) == "nil" or (type(args.min_width) == "number" and args.min_width > 0), "min_width must be nil or a number > 0")
|
||||
|
||||
-- always 1 tall
|
||||
args.height = 1
|
||||
|
||||
-- determine widths
|
||||
@@ -49,7 +48,7 @@ local function tabbar(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
assert(e.frame.w >= (button_width * #args.tabs), "graphics.elements.controls.tabbar: width insufficent to display all tabs")
|
||||
element.assert(e.frame.w >= (button_width * #args.tabs), "width insufficent to display all tabs")
|
||||
|
||||
-- default to 1st tab
|
||||
e.value = 1
|
||||
@@ -66,21 +65,21 @@ local function tabbar(args)
|
||||
end
|
||||
|
||||
-- show the tab state
|
||||
local function draw()
|
||||
function e.redraw()
|
||||
for i = 1, #args.tabs do
|
||||
local tab = args.tabs[i] ---@type tabbar_tab
|
||||
|
||||
e.window.setCursorPos(tab._start_x, 1)
|
||||
e.w_set_cur(tab._start_x, 1)
|
||||
|
||||
if e.value == i then
|
||||
e.window.setTextColor(tab.color.fgd)
|
||||
e.window.setBackgroundColor(tab.color.bkg)
|
||||
e.w_set_fgd(tab.color.fgd)
|
||||
e.w_set_bkg(tab.color.bkg)
|
||||
else
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
e.window.write(util.pad(tab.name, button_width))
|
||||
e.w_write(util.pad(tab.name, button_width))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -108,7 +107,7 @@ local function tabbar(args)
|
||||
-- tap always has identical coordinates, so this always passes for taps
|
||||
if tab_ini == tab_cur and tab_cur ~= nil then
|
||||
e.value = tab_cur
|
||||
draw()
|
||||
e.redraw()
|
||||
args.callback(e.value)
|
||||
end
|
||||
end
|
||||
@@ -118,13 +117,13 @@ local function tabbar(args)
|
||||
---@param val integer new value
|
||||
function e.set_value(val)
|
||||
e.value = val
|
||||
draw()
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
draw()
|
||||
e.redraw()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return tabbar
|
||||
|
||||
@@ -4,19 +4,22 @@ local element = require("graphics.element")
|
||||
|
||||
---@class displaybox_args
|
||||
---@field window table
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new root display box
|
||||
---@nodiscard
|
||||
---@param args displaybox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function displaybox(args)
|
||||
-- create new graphics element base object
|
||||
return element.new(args).get()
|
||||
return element.new(args).complete()
|
||||
end
|
||||
|
||||
return displaybox
|
||||
|
||||
@@ -6,11 +6,12 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new div element
|
||||
---@nodiscard
|
||||
@@ -18,7 +19,7 @@ local element = require("graphics.element")
|
||||
---@return graphics_element element, element_id id
|
||||
local function div(args)
|
||||
-- create new graphics element base object
|
||||
return element.new(args).get()
|
||||
return element.new(args).complete()
|
||||
end
|
||||
|
||||
return div
|
||||
|
||||
197
graphics/elements/form/number_field.lua
Normal file
197
graphics/elements/form/number_field.lua
Normal file
@@ -0,0 +1,197 @@
|
||||
-- Numeric Value Entry Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class number_field_args
|
||||
---@field default? number default value, defaults to 0
|
||||
---@field min? number minimum, enforced on unfocus
|
||||
---@field max? number maximum, enforced on unfocus
|
||||
---@field max_chars? integer maximum number of characters, defaults to width
|
||||
---@field max_int_digits? integer maximum number of integer digits, enforced on unfocus
|
||||
---@field max_frac_digits? integer maximum number of fractional digits, enforced on unfocus
|
||||
---@field allow_decimal? boolean true to allow decimals
|
||||
---@field allow_negative? boolean true to allow negative numbers
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new numeric entry field
|
||||
---@param args number_field_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function number_field(args)
|
||||
element.assert(args.max_int_digits == nil or (util.is_int(args.max_int_digits) and args.max_int_digits > 0), "max_int_digits must be an integer greater than zero if supplied")
|
||||
element.assert(args.max_frac_digits == nil or (util.is_int(args.max_frac_digits) and args.max_frac_digits > 0), "max_frac_digits must be an integer greater than zero if supplied")
|
||||
|
||||
args.height = 1
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local has_decimal = false
|
||||
|
||||
args.max_chars = args.max_chars or e.frame.w
|
||||
|
||||
-- set initial value
|
||||
e.value = "" .. (args.default or 0)
|
||||
|
||||
-- make an interactive field manager
|
||||
local ifield = core.new_ifield(e, args.max_chars, args.fg_bg, args.dis_fg_bg)
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled then
|
||||
if core.events.was_clicked(event.type) then
|
||||
e.take_focus()
|
||||
|
||||
if event.type == MOUSE_CLICK.UP and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
ifield.move_cursor(event.current.x)
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||
ifield.select_all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.CHAR and string.len(e.value) < args.max_chars then
|
||||
if tonumber(event.name) then
|
||||
if e.value == 0 then e.value = "" end
|
||||
ifield.try_insert_char(event.name)
|
||||
end
|
||||
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if (event.key == keys.backspace or event.key == keys.delete) and (string.len(e.value) > 0) then
|
||||
ifield.backspace()
|
||||
has_decimal = string.find(e.value, "%.") ~= nil
|
||||
elseif (event.key == keys.period or event.key == keys.numPadDecimal) and (not has_decimal) and args.allow_decimal then
|
||||
has_decimal = true
|
||||
ifield.try_insert_char(".")
|
||||
elseif (event.key == keys.minus or event.key == keys.numPadSubtract) and (string.len(e.value) == 0) and args.allow_negative then
|
||||
ifield.set_value("-")
|
||||
elseif event.key == keys.left then
|
||||
ifield.nav_left()
|
||||
elseif event.key == keys.right then
|
||||
ifield.nav_right()
|
||||
elseif event.key == keys.a and event.ctrl then
|
||||
ifield.select_all()
|
||||
elseif event.key == keys.home or event.key == keys.up then
|
||||
ifield.nav_start()
|
||||
elseif event.key == keys["end"] or event.key == keys.down then
|
||||
ifield.nav_end()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value (must be a number)
|
||||
---@param val number number to show
|
||||
function e.set_value(val)
|
||||
if tonumber(val) then ifield.set_value("" .. tonumber(val)) end
|
||||
end
|
||||
|
||||
-- set minimum input value
|
||||
---@param min integer minimum allowed value
|
||||
function e.set_min(min)
|
||||
args.min = min
|
||||
e.on_unfocused()
|
||||
end
|
||||
|
||||
-- set maximum input value
|
||||
---@param max integer maximum allowed value
|
||||
function e.set_max(max)
|
||||
args.max = max
|
||||
e.on_unfocused()
|
||||
end
|
||||
|
||||
-- replace text with pasted text if its a number
|
||||
---@param text string string pasted
|
||||
function e.handle_paste(text)
|
||||
if tonumber(text) then
|
||||
ifield.set_value("" .. tonumber(text))
|
||||
else
|
||||
ifield.set_value("0")
|
||||
end
|
||||
end
|
||||
|
||||
-- handle unfocused
|
||||
function e.on_unfocused()
|
||||
local val = tonumber(e.value)
|
||||
local max = tonumber(args.max)
|
||||
local min = tonumber(args.min)
|
||||
|
||||
if type(val) == "number" then
|
||||
if args.max_int_digits or args.max_frac_digits then
|
||||
local str = e.value
|
||||
local ceil = false
|
||||
|
||||
if string.find(str, "-") then str = string.sub(e.value, 2) end
|
||||
local parts = util.strtok(str, ".")
|
||||
|
||||
if parts[1] and args.max_int_digits then
|
||||
if string.len(parts[1]) > args.max_int_digits then
|
||||
parts[1] = string.rep("9", args.max_int_digits)
|
||||
ceil = true
|
||||
end
|
||||
end
|
||||
|
||||
if args.allow_decimal and args.max_frac_digits then
|
||||
if ceil then
|
||||
parts[2] = string.rep("9", args.max_frac_digits)
|
||||
elseif parts[2] and (string.len(parts[2]) > args.max_frac_digits) then
|
||||
-- add a half of the highest precision fractional value in order to round using floor
|
||||
local scaled = math.fmod(val, 1) * (10 ^ (args.max_frac_digits))
|
||||
local value = math.floor(scaled + 0.5)
|
||||
local unscaled = value * (10 ^ (-args.max_frac_digits))
|
||||
parts[2] = string.sub(tostring(unscaled), 3) -- remove starting "0."
|
||||
end
|
||||
end
|
||||
|
||||
if parts[2] then parts[2] = "." .. parts[2] else parts[2] = "" end
|
||||
|
||||
val = tonumber((parts[1] or "") .. parts[2])
|
||||
end
|
||||
|
||||
if type(args.max) == "number" and val > max then
|
||||
e.value = "" .. max
|
||||
ifield.nav_start()
|
||||
elseif type(args.min) == "number" and val < min then
|
||||
e.value = "" .. min
|
||||
ifield.nav_start()
|
||||
else
|
||||
e.value = "" .. val
|
||||
ifield.nav_end()
|
||||
end
|
||||
else
|
||||
e.value = ""
|
||||
end
|
||||
|
||||
ifield.show()
|
||||
end
|
||||
|
||||
-- handle focus (not unfocus), enable, and redraw with show()
|
||||
e.on_focused = ifield.show
|
||||
e.on_enabled = ifield.show
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return number_field
|
||||
105
graphics/elements/form/text_field.lua
Normal file
105
graphics/elements/form/text_field.lua
Normal file
@@ -0,0 +1,105 @@
|
||||
-- Text Value Entry Graphics Element
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class text_field_args
|
||||
---@field value? string initial value
|
||||
---@field max_len? integer maximum string length
|
||||
---@field censor? string character to replace text with when printing to screen
|
||||
---@field dis_fg_bg? cpair foreground/background colors when disabled
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new text entry field
|
||||
---@param args text_field_args
|
||||
---@return graphics_element element, element_id id, function censor_ctl
|
||||
local function text_field(args)
|
||||
args.height = 1
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- set initial value
|
||||
e.value = args.value or ""
|
||||
|
||||
-- make an interactive field manager
|
||||
local ifield = core.new_ifield(e, args.max_len or e.frame.w, args.fg_bg, args.dis_fg_bg)
|
||||
|
||||
ifield.censor(args.censor)
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
-- only handle if on an increment or decrement arrow
|
||||
if e.enabled then
|
||||
if core.events.was_clicked(event.type) then
|
||||
e.take_focus()
|
||||
|
||||
if event.type == MOUSE_CLICK.UP and e.in_frame_bounds(event.current.x, event.current.y) then
|
||||
ifield.move_cursor(event.current.x)
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOUBLE_CLICK then
|
||||
ifield.select_all()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.CHAR then
|
||||
ifield.try_insert_char(event.name)
|
||||
elseif event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if (event.key == keys.backspace or event.key == keys.delete) then
|
||||
ifield.backspace()
|
||||
elseif event.key == keys.left then
|
||||
ifield.nav_left()
|
||||
elseif event.key == keys.right then
|
||||
ifield.nav_right()
|
||||
elseif event.key == keys.a and event.ctrl then
|
||||
ifield.select_all()
|
||||
elseif event.key == keys.home or event.key == keys.up then
|
||||
ifield.nav_start()
|
||||
elseif event.key == keys["end"] or event.key == keys.down then
|
||||
ifield.nav_end()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val string string to set
|
||||
function e.set_value(val)
|
||||
ifield.set_value(val)
|
||||
end
|
||||
|
||||
-- replace text with pasted text
|
||||
---@param text string string to set
|
||||
function e.handle_paste(text)
|
||||
ifield.set_value(text)
|
||||
end
|
||||
|
||||
-- handle focus, enable, and redraw with show()
|
||||
e.on_focused = ifield.show
|
||||
e.on_unfocused = ifield.show
|
||||
e.on_enabled = ifield.show
|
||||
e.on_disabled = ifield.show
|
||||
e.redraw = ifield.show
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
local elem, id = e.complete()
|
||||
return elem, id, ifield.censor
|
||||
end
|
||||
|
||||
return text_field
|
||||
@@ -16,21 +16,22 @@ local flasher = require("graphics.flasher")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new alarm indicator light
|
||||
---@nodiscard
|
||||
---@param args alarm_indicator_light
|
||||
---@return graphics_element element, element_id id
|
||||
local function alarm_indicator_light(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.alight: label is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.alight: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.alight: c2 is a required field")
|
||||
assert(type(args.c3) == "number", "graphics.elements.indicators.alight: c3 is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.c1) == "number", "c1 is a required field")
|
||||
element.assert(type(args.c2) == "number", "c2 is a required field")
|
||||
element.assert(type(args.c3) == "number", "c3 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.alight: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
@@ -50,19 +51,21 @@ local function alarm_indicator_light(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 1
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
if e.value == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -75,7 +78,7 @@ local function alarm_indicator_light(args)
|
||||
local was_off = e.value ~= 2
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state == 2) then
|
||||
@@ -86,17 +89,17 @@ local function alarm_indicator_light(args)
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
if new_state == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -104,11 +107,16 @@ local function alarm_indicator_light(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return alarm_indicator_light
|
||||
|
||||
@@ -11,15 +11,15 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
|
||||
-- new core map box
|
||||
---@nodiscard
|
||||
---@param args core_map_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function core_map(args)
|
||||
assert(util.is_int(args.reactor_l), "graphics.elements.indicators.coremap: reactor_l is a required field")
|
||||
assert(util.is_int(args.reactor_w), "graphics.elements.indicators.coremap: reactor_w is a required field")
|
||||
element.assert(util.is_int(args.reactor_l), "reactor_l is a required field")
|
||||
element.assert(util.is_int(args.reactor_w), "reactor_w is a required field")
|
||||
|
||||
-- require max dimensions
|
||||
args.width = 18
|
||||
@@ -31,6 +31,8 @@ local function core_map(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 0
|
||||
|
||||
local alternator = true
|
||||
|
||||
local core_l = args.reactor_l - 2
|
||||
@@ -47,25 +49,25 @@ local function core_map(args)
|
||||
|
||||
-- create coordinate grid and frame
|
||||
local function draw_frame()
|
||||
e.window.setTextColor(colors.white)
|
||||
e.w_set_fgd(colors.white)
|
||||
|
||||
for x = 0, (inner_width - 1) do
|
||||
e.window.setCursorPos(x + start_x, 1)
|
||||
e.window.write(util.sprintf("%X", x))
|
||||
e.w_set_cur(x + start_x, 1)
|
||||
e.w_write(util.sprintf("%X", x))
|
||||
end
|
||||
|
||||
for y = 0, (inner_height - 1) do
|
||||
e.window.setCursorPos(1, y + start_y)
|
||||
e.window.write(util.sprintf("%X", y))
|
||||
e.w_set_cur(1, y + start_y)
|
||||
e.w_write(util.sprintf("%X", y))
|
||||
end
|
||||
|
||||
-- even out bottom edge
|
||||
e.window.setTextColor(e.fg_bg.bkg)
|
||||
e.window.setBackgroundColor(args.parent.get_fg_bg().bkg)
|
||||
e.window.setCursorPos(1, e.frame.h)
|
||||
e.window.write(util.strrep("\x8f", e.frame.w))
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.w_set_fgd(e.fg_bg.bkg)
|
||||
e.w_set_bkg(args.parent.get_fg_bg().bkg)
|
||||
e.w_set_cur(1, e.frame.h)
|
||||
e.w_write(string.rep("\x8f", e.frame.w))
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- draw the core
|
||||
@@ -102,13 +104,13 @@ local function core_map(args)
|
||||
|
||||
-- draw pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.window.setCursorPos(start_x, y)
|
||||
e.w_set_cur(start_x, y)
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
i = i + 1
|
||||
e.window.blit("\x07", text_c, back_c)
|
||||
e.w_blit("\x07", text_c, back_c)
|
||||
else
|
||||
e.window.blit("\x07", "7", "8")
|
||||
e.w_blit("\x07", "7", "8")
|
||||
end
|
||||
|
||||
alternator = not alternator
|
||||
@@ -157,13 +159,16 @@ local function core_map(args)
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial (one-time except for resize()) frame draw
|
||||
draw_frame()
|
||||
-- redraw both frame and core
|
||||
function e.redraw()
|
||||
draw_frame()
|
||||
draw_core(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.on_update(0)
|
||||
e.redraw()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return core_map
|
||||
|
||||
@@ -14,37 +14,31 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new data indicator
|
||||
---@nodiscard
|
||||
---@param args data_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function data(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.data: label is a required field")
|
||||
assert(type(args.format) == "string", "graphics.elements.indicators.data: format is a required field")
|
||||
assert(args.value ~= nil, "graphics.elements.indicators.data: value is a required field")
|
||||
assert(util.is_int(args.width), "graphics.elements.indicators.data: width is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.format) == "string", "format is a required field")
|
||||
element.assert(args.value ~= nil, "value is a required field")
|
||||
element.assert(util.is_int(args.width), "width is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- label color
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_a)
|
||||
end
|
||||
e.value = args.value
|
||||
|
||||
-- write label
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
local label_len = string.len(args.label)
|
||||
local data_start = 1
|
||||
local value_color = e.fg_bg.fgd
|
||||
local label_len = string.len(args.label)
|
||||
local data_start = 1
|
||||
local clear_width = args.width
|
||||
|
||||
if label_len > 0 then
|
||||
@@ -58,25 +52,25 @@ local function data(args)
|
||||
e.value = value
|
||||
|
||||
-- clear old data and label
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.write(util.spaces(clear_width))
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_write(util.spaces(clear_width))
|
||||
|
||||
-- write data
|
||||
local data_str = util.sprintf(args.format, value)
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(value_color)
|
||||
if args.commas then
|
||||
e.window.write(util.comma_format(data_str))
|
||||
e.w_write(util.comma_format(data_str))
|
||||
else
|
||||
e.window.write(data_str)
|
||||
e.w_write(data_str)
|
||||
end
|
||||
|
||||
-- write label
|
||||
if args.unit ~= nil then
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_b)
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
e.window.write(" " .. args.unit)
|
||||
e.w_write(" " .. args.unit)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -84,10 +78,26 @@ local function data(args)
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(args.value)
|
||||
-- change the foreground color of the value, or all text if no label/unit colors provided
|
||||
---@param c color
|
||||
function e.recolor(c)
|
||||
value_color = c
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return data
|
||||
|
||||
@@ -10,27 +10,29 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new horizontal bar
|
||||
---@nodiscard
|
||||
---@param args hbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function hbar(args)
|
||||
-- properties/state
|
||||
local last_num_bars = -1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 0.0
|
||||
|
||||
-- bar width is width - 5 characters for " 100%" if showing percent
|
||||
local bar_width = util.trinary(args.show_percent, e.frame.w - 5, e.frame.w)
|
||||
|
||||
assert(bar_width > 0, "graphics.elements.indicators.hbar: too small for bar")
|
||||
element.assert(bar_width > 0, "too small for bar")
|
||||
|
||||
local last_num_bars = -1
|
||||
|
||||
-- determine bar colors
|
||||
local bar_bkg = e.fg_bg.blit_bkg
|
||||
@@ -86,16 +88,16 @@ local function hbar(args)
|
||||
|
||||
-- draw bar
|
||||
for y = 1, e.frame.h do
|
||||
e.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
-- intentionally swapped fgd/bkg since we use spaces as fill, but they are the opposite
|
||||
e.window.blit(spaces, bkg, fgd)
|
||||
e.w_blit(spaces, bkg, fgd)
|
||||
end
|
||||
end
|
||||
|
||||
-- update percentage
|
||||
if args.show_percent then
|
||||
e.window.setCursorPos(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
|
||||
e.window.write(util.sprintf("%3.0f%%", fraction * 100))
|
||||
e.w_set_cur(bar_width + 2, math.max(1, math.ceil(e.frame.h / 2)))
|
||||
e.w_write(util.sprintf("%3.0f%%", fraction * 100))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -104,22 +106,23 @@ local function hbar(args)
|
||||
function e.recolor(bar_fg_bg)
|
||||
bar_bkg = bar_fg_bg.blit_bkg
|
||||
bar_fgd = bar_fg_bg.blit_fgd
|
||||
|
||||
-- re-draw
|
||||
last_num_bars = 0
|
||||
if type(e.value) == "number" then
|
||||
e.on_update(e.value)
|
||||
end
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- set the percentage value
|
||||
---@param val number 0.0 to 1.0
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initialize to 0
|
||||
e.on_update(0)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
last_num_bars = -1
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return hbar
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
-- Icon Indicator Graphics Element
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local element = require("graphics.element")
|
||||
|
||||
---@class icon_sym_color
|
||||
@@ -16,26 +14,26 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new icon indicator
|
||||
---@nodiscard
|
||||
---@param args icon_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function icon(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.icon: label is a required field")
|
||||
assert(type(args.states) == "table", "graphics.elements.indicators.icon: states is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.states) == "table", "states is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 4
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = args.value or 1
|
||||
|
||||
-- state blit strings
|
||||
local state_blit_cmds = {}
|
||||
for i = 1, #args.states do
|
||||
@@ -43,32 +41,36 @@ local function icon(args)
|
||||
|
||||
table.insert(state_blit_cmds, {
|
||||
text = " " .. sym_color.symbol .. " ",
|
||||
fgd = util.strrep(sym_color.color.blit_fgd, 3),
|
||||
bkg = util.strrep(sym_color.color.blit_bkg, 3)
|
||||
fgd = string.rep(sym_color.color.blit_fgd, 3),
|
||||
bkg = string.rep(sym_color.color.blit_bkg, 3)
|
||||
})
|
||||
end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.window.setCursorPos(5, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local blit_cmd = state_blit_cmds[new_state]
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial icon draw
|
||||
e.on_update(args.value or 1)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
e.w_set_cur(5, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
return e.get()
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return icon
|
||||
|
||||
@@ -14,41 +14,40 @@ local flasher = require("graphics.flasher")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new indicator LED
|
||||
---@nodiscard
|
||||
---@param args indicator_led_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.led: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.led: colors is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.colors) == "table", "colors is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.led: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = false
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -60,8 +59,8 @@ local function indicator_led(args)
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
else
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit("\x8c", args.colors.blit_a, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -72,8 +71,8 @@ local function indicator_led(args)
|
||||
flasher.stop(flash_callback)
|
||||
end
|
||||
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit("\x8c", args.colors.blit_b, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
@@ -87,14 +86,19 @@ local function indicator_led(args)
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(false)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_led
|
||||
|
||||
@@ -16,33 +16,29 @@ local flasher = require("graphics.flasher")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new dual LED indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_led_pair_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_pair(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.ledpair: label is a required field")
|
||||
assert(type(args.off) == "number", "graphics.elements.indicators.ledpair: off is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.ledpair: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.ledpair: c2 is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.off) == "number", "off is a required field")
|
||||
element.assert(type(args.c1) == "number", "c1 is a required field")
|
||||
element.assert(type(args.c2) == "number", "c2 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.ledpair: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- blit translations
|
||||
local co = colors.toBlit(args.off)
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
@@ -50,21 +46,20 @@ local function indicator_led_pair(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -76,7 +71,7 @@ local function indicator_led_pair(args)
|
||||
local was_off = e.value <= 1
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state > 1) then
|
||||
@@ -85,15 +80,14 @@ local function indicator_led_pair(args)
|
||||
elseif new_state <= 1 then
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c1, e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", c2, e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", co, e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -101,14 +95,19 @@ local function indicator_led_pair(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_led_pair
|
||||
|
||||
@@ -9,36 +9,33 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new RGB LED indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_led_rgb_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_led_rgb(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.ledrgb: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.ledrgb: colors is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.colors) == "table", "colors is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 0, string.len(args.label)) + 2
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
if type(args.colors[new_state]) == "number" then
|
||||
e.window.blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
||||
e.w_blit("\x8c", colors.toBlit(args.colors[new_state]), e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,14 +43,19 @@ local function indicator_led_rgb(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
if string.len(args.label) > 0 then
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(e.value)
|
||||
if string.len(args.label) > 0 then
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_led_rgb
|
||||
|
||||
@@ -14,41 +14,40 @@ local flasher = require("graphics.flasher")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new indicator light
|
||||
---@nodiscard
|
||||
---@param args indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function indicator_light(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.light: label is a required field")
|
||||
assert(type(args.colors) == "table", "graphics.elements.indicators.light: colors is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.colors) == "table", "colors is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.light: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = false
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -60,8 +59,8 @@ local function indicator_light(args)
|
||||
flash_on = true
|
||||
flasher.start(flash_callback, args.period)
|
||||
else
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_a, args.colors.blit_a .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -72,8 +71,8 @@ local function indicator_light(args)
|
||||
flasher.stop(flash_callback)
|
||||
end
|
||||
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(" \x95", "0" .. args.colors.blit_b, args.colors.blit_b .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
@@ -87,12 +86,17 @@ local function indicator_light(args)
|
||||
---@param val boolean indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(false)
|
||||
e.window.setCursorPos(3, 1)
|
||||
e.window.write(args.label)
|
||||
-- draw label and indicator light
|
||||
function e.redraw()
|
||||
e.on_update(false)
|
||||
e.w_set_cur(3, 1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return indicator_light
|
||||
|
||||
@@ -9,39 +9,31 @@ local element = require("graphics.element")
|
||||
---@field format string power format override (lua string format)
|
||||
---@field rate boolean? whether to append /t to the end (power per tick)
|
||||
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
|
||||
---@field value any default value
|
||||
---@field value number default value
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new power indicator
|
||||
---@nodiscard
|
||||
---@param args power_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function power(args)
|
||||
assert(args.value ~= nil, "graphics.elements.indicators.power: value is a required field")
|
||||
assert(util.is_int(args.width), "graphics.elements.indicators.power: width is a required field")
|
||||
element.assert(type(args.value) == "number", "value is a required field")
|
||||
element.assert(util.is_int(args.width), "width is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- label color
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_a)
|
||||
end
|
||||
e.value = args.value
|
||||
|
||||
-- write label
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
|
||||
local data_start = string.len(args.label) + 2
|
||||
if string.len(args.label) == 0 then data_start = 1 end
|
||||
local data_start = 0
|
||||
|
||||
-- on state change
|
||||
---@param value any new value
|
||||
@@ -51,13 +43,13 @@ local function power(args)
|
||||
local data_str, unit = util.power_format(value, false, args.format)
|
||||
|
||||
-- write data
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.window.write(util.comma_format(data_str))
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_write(util.comma_format(data_str))
|
||||
|
||||
-- write unit
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_b)
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
|
||||
-- append per tick if rate is set
|
||||
@@ -69,17 +61,29 @@ local function power(args)
|
||||
if unit == "FE" then unit = "FE " end
|
||||
end
|
||||
|
||||
e.window.write(" " .. unit)
|
||||
e.w_write(" " .. unit)
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(args.value)
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
return e.get()
|
||||
data_start = string.len(args.label) + 2
|
||||
if string.len(args.label) == 0 then data_start = 1 end
|
||||
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return power
|
||||
|
||||
@@ -10,37 +10,30 @@ local element = require("graphics.element")
|
||||
---@field format string data format (lua string format)
|
||||
---@field commas? boolean whether to use commas if a number is given (default to false)
|
||||
---@field lu_colors? cpair label foreground color (a), unit foreground color (b)
|
||||
---@field value any default value
|
||||
---@field value? radiation_reading default value
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width integer length
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new radiation indicator
|
||||
---@nodiscard
|
||||
---@param args rad_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function rad(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.rad: label is a required field")
|
||||
assert(type(args.format) == "string", "graphics.elements.indicators.rad: format is a required field")
|
||||
assert(util.is_int(args.width), "graphics.elements.indicators.rad: width is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.format) == "string", "format is a required field")
|
||||
element.assert(util.is_int(args.width), "width is a required field")
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- label color
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_a)
|
||||
end
|
||||
|
||||
-- write label
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.write(args.label)
|
||||
e.value = args.value or types.new_zero_radiation_reading()
|
||||
|
||||
local label_len = string.len(args.label)
|
||||
local data_start = 1
|
||||
@@ -57,34 +50,43 @@ local function rad(args)
|
||||
e.value = value.radiation
|
||||
|
||||
-- clear old data and label
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.write(util.spaces(clear_width))
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_write(util.spaces(clear_width))
|
||||
|
||||
-- write data
|
||||
local data_str = util.sprintf(args.format, e.value)
|
||||
e.window.setCursorPos(data_start, 1)
|
||||
e.window.setTextColor(e.fg_bg.fgd)
|
||||
e.w_set_cur(data_start, 1)
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
if args.commas then
|
||||
e.window.write(util.comma_format(data_str))
|
||||
e.w_write(util.comma_format(data_str))
|
||||
else
|
||||
e.window.write(data_str)
|
||||
e.w_write(data_str)
|
||||
end
|
||||
|
||||
-- write unit
|
||||
if args.lu_colors ~= nil then
|
||||
e.window.setTextColor(args.lu_colors.color_b)
|
||||
e.w_set_fgd(args.lu_colors.color_b)
|
||||
end
|
||||
e.window.write(" " .. value.unit)
|
||||
e.w_write(" " .. value.unit)
|
||||
end
|
||||
|
||||
-- set the value
|
||||
---@param val any new value
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial value draw
|
||||
e.on_update(types.new_zero_radiation_reading())
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if args.lu_colors ~= nil then e.w_set_fgd(args.lu_colors.color_a) end
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_write(args.label)
|
||||
|
||||
return e.get()
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return rad
|
||||
|
||||
@@ -15,25 +15,22 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field height? integer 1 if omitted, must be an odd number
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new state indicator
|
||||
---@nodiscard
|
||||
---@param args state_indicator_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function state_indicator(args)
|
||||
assert(type(args.states) == "table", "graphics.elements.indicators.state: states is a required field")
|
||||
element.assert(type(args.states) == "table", "states is a required field")
|
||||
|
||||
-- determine height
|
||||
if util.is_int(args.height) then
|
||||
assert(args.height % 2 == 1, "graphics.elements.indicators.state: height should be an odd number")
|
||||
else
|
||||
args.height = 1
|
||||
end
|
||||
element.assert(args.height % 2 == 1, "height should be an odd number")
|
||||
else args.height = 1 end
|
||||
|
||||
-- initial guess at width
|
||||
args.width = args.min_width or 1
|
||||
|
||||
-- state blit strings
|
||||
@@ -41,7 +38,6 @@ local function state_indicator(args)
|
||||
for i = 1, #args.states do
|
||||
local state_def = args.states[i] ---@type state_text_color
|
||||
|
||||
-- re-determine width
|
||||
if string.len(state_def.text) > args.width then
|
||||
args.width = string.len(state_def.text)
|
||||
end
|
||||
@@ -50,21 +46,28 @@ local function state_indicator(args)
|
||||
|
||||
table.insert(state_blit_cmds, {
|
||||
text = text,
|
||||
fgd = util.strrep(state_def.color.blit_fgd, string.len(text)),
|
||||
bkg = util.strrep(state_def.color.blit_bkg, string.len(text))
|
||||
fgd = string.rep(state_def.color.blit_fgd, string.len(text)),
|
||||
bkg = string.rep(state_def.color.blit_bkg, string.len(text))
|
||||
})
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = args.value or 1
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
local blit_cmd = state_blit_cmds[e.value]
|
||||
e.w_set_cur(1, 1)
|
||||
e.w_blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
end
|
||||
|
||||
-- on state change
|
||||
---@param new_state integer indicator state
|
||||
function e.on_update(new_state)
|
||||
local blit_cmd = state_blit_cmds[new_state]
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.window.blit(blit_cmd.text, blit_cmd.fgd, blit_cmd.bkg)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- set indicator state
|
||||
@@ -72,9 +75,9 @@ local function state_indicator(args)
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- initial draw
|
||||
e.on_update(args.value or 1)
|
||||
e.redraw()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return state_indicator
|
||||
|
||||
@@ -16,55 +16,50 @@ local flasher = require("graphics.flasher")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tri-state indicator light
|
||||
---@nodiscard
|
||||
---@param args tristate_indicator_light_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tristate_indicator_light(args)
|
||||
assert(type(args.label) == "string", "graphics.elements.indicators.trilight: label is a required field")
|
||||
assert(type(args.c1) == "number", "graphics.elements.indicators.trilight: c1 is a required field")
|
||||
assert(type(args.c2) == "number", "graphics.elements.indicators.trilight: c2 is a required field")
|
||||
assert(type(args.c3) == "number", "graphics.elements.indicators.trilight: c3 is a required field")
|
||||
element.assert(type(args.label) == "string", "label is a required field")
|
||||
element.assert(type(args.c1) == "number", "c1 is a required field")
|
||||
element.assert(type(args.c2) == "number", "c2 is a required field")
|
||||
element.assert(type(args.c3) == "number", "c3 is a required field")
|
||||
|
||||
if args.flash then
|
||||
assert(util.is_int(args.period), "graphics.elements.indicators.trilight: period is a required field if flash is enabled")
|
||||
element.assert(util.is_int(args.period), "period is a required field if flash is enabled")
|
||||
end
|
||||
|
||||
-- single line
|
||||
args.height = 1
|
||||
|
||||
-- determine width
|
||||
args.width = math.max(args.min_label_width or 1, string.len(args.label)) + 2
|
||||
|
||||
-- flasher state
|
||||
local flash_on = true
|
||||
|
||||
-- blit translations
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
local c3 = colors.toBlit(args.c3)
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- init value for initial check in on_update
|
||||
e.value = 1
|
||||
|
||||
local flash_on = true
|
||||
|
||||
local c1 = colors.toBlit(args.c1)
|
||||
local c2 = colors.toBlit(args.c2)
|
||||
local c3 = colors.toBlit(args.c3)
|
||||
|
||||
-- called by flasher when enabled
|
||||
local function flash_callback()
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if flash_on then
|
||||
if e.value == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif e.value == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
|
||||
flash_on = not flash_on
|
||||
@@ -76,7 +71,7 @@ local function tristate_indicator_light(args)
|
||||
local was_off = e.value <= 1
|
||||
|
||||
e.value = new_state
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
if args.flash then
|
||||
if was_off and (new_state > 1) then
|
||||
@@ -86,14 +81,14 @@ local function tristate_indicator_light(args)
|
||||
flash_on = false
|
||||
flasher.stop(flash_callback)
|
||||
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
elseif new_state == 2 then
|
||||
e.window.blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c2, c2 .. e.fg_bg.blit_bkg)
|
||||
elseif new_state == 3 then
|
||||
e.window.blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c3, c3 .. e.fg_bg.blit_bkg)
|
||||
else
|
||||
e.window.blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
e.w_blit(" \x95", "0" .. c1, c1 .. e.fg_bg.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -101,11 +96,16 @@ local function tristate_indicator_light(args)
|
||||
---@param val integer indicator state
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
-- write label and initial indicator light
|
||||
e.on_update(1)
|
||||
e.window.write(args.label)
|
||||
-- draw light and label
|
||||
function e.redraw()
|
||||
e.on_update(1)
|
||||
e.w_write(args.label)
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return tristate_indicator_light
|
||||
|
||||
@@ -8,29 +8,30 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new vertical bar
|
||||
---@nodiscard
|
||||
---@param args vbar_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function vbar(args)
|
||||
-- properties/state
|
||||
local last_num_bars = -1
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- blit strings
|
||||
local fgd = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local bkg = util.strrep(e.fg_bg.blit_bkg, e.frame.w)
|
||||
e.value = 0.0
|
||||
|
||||
local last_num_bars = -1
|
||||
|
||||
local fgd = string.rep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local bkg = string.rep(e.fg_bg.blit_bkg, e.frame.w)
|
||||
local spaces = util.spaces(e.frame.w)
|
||||
local one_third = util.strrep("\x8f", e.frame.w)
|
||||
local two_thirds = util.strrep("\x83", e.frame.w)
|
||||
local one_third = string.rep("\x8f", e.frame.w)
|
||||
local two_thirds = string.rep("\x83", e.frame.w)
|
||||
|
||||
-- handle data changes
|
||||
---@param fraction number 0.0 to 1.0
|
||||
@@ -51,55 +52,56 @@ local function vbar(args)
|
||||
if num_bars ~= last_num_bars then
|
||||
last_num_bars = num_bars
|
||||
|
||||
-- start bottom up
|
||||
local y = e.frame.h
|
||||
|
||||
-- start at base of vertical bar
|
||||
e.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
|
||||
-- fill percentage
|
||||
for _ = 1, num_bars / 3 do
|
||||
e.window.blit(spaces, bkg, fgd)
|
||||
e.w_blit(spaces, bkg, fgd)
|
||||
y = y - 1
|
||||
e.window.setCursorPos(1, y)
|
||||
e.w_set_cur(1, y)
|
||||
end
|
||||
|
||||
-- add fractional bar if needed
|
||||
if num_bars % 3 == 1 then
|
||||
e.window.blit(one_third, bkg, fgd)
|
||||
e.w_blit(one_third, bkg, fgd)
|
||||
y = y - 1
|
||||
elseif num_bars % 3 == 2 then
|
||||
e.window.blit(two_thirds, bkg, fgd)
|
||||
e.w_blit(two_thirds, bkg, fgd)
|
||||
y = y - 1
|
||||
end
|
||||
|
||||
-- fill the rest blank
|
||||
while y > 0 do
|
||||
e.window.setCursorPos(1, y)
|
||||
e.window.blit(spaces, fgd, bkg)
|
||||
e.w_set_cur(1, y)
|
||||
e.w_blit(spaces, fgd, bkg)
|
||||
y = y - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- change bar color
|
||||
---@param fg_bg cpair new bar colors
|
||||
function e.recolor(fg_bg)
|
||||
fgd = util.strrep(fg_bg.blit_fgd, e.frame.w)
|
||||
bkg = util.strrep(fg_bg.blit_bkg, e.frame.w)
|
||||
|
||||
-- re-draw
|
||||
last_num_bars = 0
|
||||
if type(e.value) == "number" then
|
||||
e.on_update(e.value)
|
||||
end
|
||||
end
|
||||
|
||||
-- set the percentage value
|
||||
---@param val number 0.0 to 1.0
|
||||
function e.set_value(val) e.on_update(val) end
|
||||
|
||||
return e.get()
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
last_num_bars = -1
|
||||
e.on_update(e.value)
|
||||
end
|
||||
|
||||
-- change bar color
|
||||
---@param fg_bg cpair new bar colors
|
||||
function e.recolor(fg_bg)
|
||||
fgd = string.rep(fg_bg.blit_fgd, e.frame.w)
|
||||
bkg = string.rep(fg_bg.blit_bkg, e.frame.w)
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return vbar
|
||||
|
||||
343
graphics/elements/listbox.lua
Normal file
343
graphics/elements/listbox.lua
Normal file
@@ -0,0 +1,343 @@
|
||||
-- Scroll-able List Box Display Graphics Element
|
||||
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local KEY_CLICK = core.events.KEY_CLICK
|
||||
local MOUSE_CLICK = core.events.MOUSE_CLICK
|
||||
|
||||
---@class listbox_args
|
||||
---@field scroll_height integer height of internal scrolling container (must fit all elements vertically tiled)
|
||||
---@field item_pad? integer spacing (lines) between items in the list (default 0)
|
||||
---@field nav_fg_bg? cpair foreground/background colors for scroll arrows and bar area
|
||||
---@field nav_active? cpair active colors for bar held down or arrow held down
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
---@class listbox_item
|
||||
---@field id string|integer element ID
|
||||
---@field e graphics_element element
|
||||
---@field y integer y position
|
||||
---@field h integer element height
|
||||
|
||||
-- new listbox element
|
||||
---@nodiscard
|
||||
---@param args listbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function listbox(args)
|
||||
args.can_focus = true
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- create content window for child elements
|
||||
local scroll_frame = window.create(e.window, 1, 1, e.frame.w - 1, args.scroll_height, false)
|
||||
e.content_window = scroll_frame
|
||||
|
||||
-- item list and scroll management
|
||||
local list = {}
|
||||
local item_pad = args.item_pad or 0
|
||||
local scroll_offset = 0
|
||||
local content_height = 0
|
||||
local max_down_scroll = 0
|
||||
-- bar control/tracking variables
|
||||
local max_bar_height = e.frame.h - 2
|
||||
local bar_height = 0 -- full height of bar
|
||||
local bar_bounds = { 0, 0 } -- top and bottom of bar
|
||||
local bar_is_scaled = false -- if the scrollbar doesn't have a 1:1 ratio with lines
|
||||
local holding_bar = false -- bar is being held by mouse
|
||||
local bar_grip_pos = 0 -- where the bar was gripped by mouse down
|
||||
local mouse_last_y = 0 -- last reported y coordinate of drag
|
||||
|
||||
-- draw scroll bar arrows, optionally showing one of them as pressed
|
||||
---@param pressed_arrow? 1|0|-1 arrow to show as pressed (1 = scroll up, 0 = neither, -1 = scroll down)
|
||||
local function draw_arrows(pressed_arrow)
|
||||
local nav_fg_bg = args.nav_fg_bg or e.fg_bg
|
||||
local active_fg_bg = args.nav_active or nav_fg_bg
|
||||
|
||||
-- draw up/down arrows
|
||||
if pressed_arrow == 1 then
|
||||
e.w_set_fgd(active_fg_bg.fgd)
|
||||
e.w_set_bkg(active_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, 1)
|
||||
e.w_write("\x1e")
|
||||
e.w_set_fgd(nav_fg_bg.fgd)
|
||||
e.w_set_bkg(nav_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, e.frame.h)
|
||||
e.w_write("\x1f")
|
||||
elseif pressed_arrow == -1 then
|
||||
e.w_set_fgd(nav_fg_bg.fgd)
|
||||
e.w_set_bkg(nav_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, 1)
|
||||
e.w_write("\x1e")
|
||||
e.w_set_fgd(active_fg_bg.fgd)
|
||||
e.w_set_bkg(active_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, e.frame.h)
|
||||
e.w_write("\x1f")
|
||||
else
|
||||
e.w_set_fgd(nav_fg_bg.fgd)
|
||||
e.w_set_bkg(nav_fg_bg.bkg)
|
||||
e.w_set_cur(e.frame.w, 1)
|
||||
e.w_write("\x1e")
|
||||
e.w_set_cur(e.frame.w, e.frame.h)
|
||||
e.w_write("\x1f")
|
||||
end
|
||||
|
||||
e.w_set_fgd(e.fg_bg.fgd)
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- render the scroll bar and re-cacluate height & bounds
|
||||
local function draw_bar()
|
||||
local offset = 2 + math.abs(scroll_offset)
|
||||
|
||||
bar_height = math.min(max_bar_height + max_down_scroll, max_bar_height)
|
||||
|
||||
if bar_height < 1 then
|
||||
bar_is_scaled = true
|
||||
-- can't do a 1:1 ratio
|
||||
-- use minimum size bar with scaled offset
|
||||
local scroll_progress = scroll_offset / max_down_scroll
|
||||
offset = 2 + math.floor(scroll_progress * (max_bar_height - 1))
|
||||
bar_height = 1
|
||||
else
|
||||
bar_is_scaled = false
|
||||
end
|
||||
|
||||
bar_bounds = { offset, (bar_height + offset) - 1 }
|
||||
|
||||
for i = 2, e.frame.h - 1 do
|
||||
if (i >= offset and i < (bar_height + offset)) and (bar_height ~= max_bar_height) then
|
||||
if args.nav_fg_bg ~= nil then
|
||||
e.w_set_bkg(args.nav_fg_bg.fgd)
|
||||
else
|
||||
e.w_set_bkg(e.fg_bg.fgd)
|
||||
end
|
||||
else
|
||||
if args.nav_fg_bg ~= nil then
|
||||
e.w_set_bkg(args.nav_fg_bg.bkg)
|
||||
else
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
end
|
||||
|
||||
e.w_set_cur(e.frame.w, i)
|
||||
if e.is_focused() then e.w_write("\x7f") else e.w_write(" ") end
|
||||
end
|
||||
|
||||
e.w_set_bkg(e.fg_bg.bkg)
|
||||
end
|
||||
|
||||
-- update item y positions and move elements
|
||||
local function update_positions()
|
||||
local next_y = 1
|
||||
|
||||
scroll_frame.setVisible(false)
|
||||
scroll_frame.setBackgroundColor(e.fg_bg.bkg)
|
||||
scroll_frame.setTextColor(e.fg_bg.fgd)
|
||||
scroll_frame.clear()
|
||||
|
||||
for i = 1, #list do
|
||||
local item = list[i] ---@type listbox_item
|
||||
item.y = next_y
|
||||
next_y = next_y + item.h + item_pad
|
||||
item.e.reposition(1, item.y)
|
||||
item.e.show()
|
||||
end
|
||||
|
||||
content_height = next_y
|
||||
max_down_scroll = math.min(-1 * (content_height - (e.frame.h + 1 + item_pad)), 0)
|
||||
if scroll_offset < max_down_scroll then scroll_offset = max_down_scroll end
|
||||
|
||||
scroll_frame.reposition(1, 1 + scroll_offset)
|
||||
scroll_frame.setVisible(true)
|
||||
|
||||
-- shift mouse events
|
||||
e.mouse_window_shift.y = scroll_offset
|
||||
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
-- determine where to scroll to based on a scrollbar being dragged without a 1:1 relationship
|
||||
---@param direction -1|1 negative 1 to scroll up by one, positive 1 to scroll down by one
|
||||
local function scaled_bar_scroll(direction)
|
||||
local scroll_progress = scroll_offset / max_down_scroll
|
||||
local bar_position = math.floor(scroll_progress * (max_bar_height - 1))
|
||||
|
||||
-- check what moving the scroll bar up or down would mean for the scroll progress
|
||||
scroll_progress = (bar_position + direction) / (max_bar_height - 1)
|
||||
|
||||
return math.max(math.floor(scroll_progress * max_down_scroll), max_down_scroll)
|
||||
end
|
||||
|
||||
-- scroll down the list
|
||||
local function scroll_down(scaled)
|
||||
if scroll_offset > max_down_scroll then
|
||||
if scaled then
|
||||
scroll_offset = scaled_bar_scroll(1)
|
||||
else
|
||||
scroll_offset = scroll_offset - 1
|
||||
end
|
||||
|
||||
update_positions()
|
||||
end
|
||||
end
|
||||
|
||||
-- scroll up the list
|
||||
local function scroll_up(scaled)
|
||||
if scroll_offset < 0 then
|
||||
if scaled then
|
||||
scroll_offset = scaled_bar_scroll(-1)
|
||||
else
|
||||
scroll_offset = scroll_offset + 1
|
||||
end
|
||||
|
||||
update_positions()
|
||||
end
|
||||
end
|
||||
|
||||
-- handle a child element having been added to the list
|
||||
---@param id element_id element identifier
|
||||
---@param child graphics_element child element
|
||||
function e.on_added(id, child)
|
||||
table.insert(list, { id = id, e = child, y = 0, h = child.get_height() })
|
||||
update_positions()
|
||||
end
|
||||
|
||||
-- handle a child element having been removed from the list
|
||||
---@param id element_id element identifier
|
||||
function e.on_removed(id)
|
||||
for idx, elem in ipairs(list) do
|
||||
if elem.id == id then
|
||||
table.remove(list, idx)
|
||||
update_positions()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle focus
|
||||
e.on_focused = draw_bar
|
||||
e.on_unfocused = draw_bar
|
||||
|
||||
-- handle a child in the list being focused, make sure it is visible
|
||||
function e.on_child_focused(child)
|
||||
for i = 1, #list do
|
||||
local item = list[i] ---@type listbox_item
|
||||
if item.e == child then
|
||||
if (item.y + scroll_offset) <= 0 then
|
||||
scroll_offset = 1 - item.y
|
||||
update_positions()
|
||||
draw_bar()
|
||||
elseif (item.y + scroll_offset) == 1 then
|
||||
-- do nothing, it's right at the top (if the bottom doesn't fit we can't easily fix that)
|
||||
elseif ((item.h + item.y - 1) + scroll_offset) > e.frame.h then
|
||||
scroll_offset = 1 - ((item.h + item.y) - e.frame.h)
|
||||
update_positions()
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle mouse interaction
|
||||
---@param event mouse_interaction mouse event
|
||||
function e.handle_mouse(event)
|
||||
if e.enabled then
|
||||
if event.type == MOUSE_CLICK.TAP then
|
||||
if event.current.x == e.frame.w then
|
||||
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
||||
scroll_up()
|
||||
if event.current.y == 1 then
|
||||
draw_arrows(1)
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||
end
|
||||
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
||||
scroll_down()
|
||||
if event.current.y == e.frame.h then
|
||||
draw_arrows(-1)
|
||||
if args.nav_active ~= nil then tcd.dispatch(0.25, function () draw_arrows(0) end) end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.DOWN then
|
||||
if event.current.x == e.frame.w then
|
||||
if event.current.y == 1 or event.current.y < bar_bounds[1] then
|
||||
scroll_up()
|
||||
if event.current.y == 1 then draw_arrows(1) end
|
||||
elseif event.current.y == e.frame.h or event.current.y > bar_bounds[2] then
|
||||
scroll_down()
|
||||
if event.current.y == e.frame.h then draw_arrows(-1) end
|
||||
else
|
||||
-- clicked on bar
|
||||
holding_bar = true
|
||||
bar_grip_pos = event.current.y - bar_bounds[1]
|
||||
mouse_last_y = event.current.y
|
||||
end
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.UP then
|
||||
holding_bar = false
|
||||
draw_arrows(0)
|
||||
elseif event.type == MOUSE_CLICK.DRAG then
|
||||
if holding_bar then
|
||||
-- if mouse is within vertical frame, including the grip point
|
||||
if event.current.y > (1 + bar_grip_pos) and event.current.y <= ((e.frame.h - bar_height) + bar_grip_pos) then
|
||||
if event.current.y < mouse_last_y then
|
||||
scroll_up(bar_is_scaled)
|
||||
elseif event.current.y > mouse_last_y then
|
||||
scroll_down(bar_is_scaled)
|
||||
end
|
||||
|
||||
mouse_last_y = event.current.y
|
||||
end
|
||||
end
|
||||
elseif event.type == MOUSE_CLICK.SCROLL_DOWN then
|
||||
scroll_down()
|
||||
elseif event.type == MOUSE_CLICK.SCROLL_UP then
|
||||
scroll_up()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- handle keyboard interaction
|
||||
---@param event key_interaction key event
|
||||
function e.handle_key(event)
|
||||
if event.type == KEY_CLICK.DOWN or event.type == KEY_CLICK.HELD then
|
||||
if event.key == keys.up then
|
||||
scroll_up()
|
||||
elseif event.key == keys.down then
|
||||
scroll_down()
|
||||
elseif event.key == keys.home then
|
||||
scroll_offset = 0
|
||||
update_positions()
|
||||
elseif event.key == keys["end"] then
|
||||
scroll_offset = max_down_scroll
|
||||
update_positions()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
draw_arrows(0)
|
||||
draw_bar()
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return listbox
|
||||
@@ -7,36 +7,44 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new multipane element
|
||||
---@nodiscard
|
||||
---@param args multipane_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function multipane(args)
|
||||
assert(type(args.panes) == "table", "graphics.elements.multipane: panes is a required field")
|
||||
element.assert(type(args.panes) == "table", "panes is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
e.value = 1
|
||||
|
||||
-- show the selected pane
|
||||
function e.redraw()
|
||||
for i = 1, #args.panes do args.panes[i].hide() end
|
||||
args.panes[e.value].show()
|
||||
end
|
||||
|
||||
-- select which pane is shown
|
||||
---@param value integer pane to show
|
||||
function e.set_value(value)
|
||||
if (e.value ~= value) and (value > 0) and (value <= #args.panes) then
|
||||
e.value = value
|
||||
|
||||
for i = 1, #args.panes do args.panes[i].hide() end
|
||||
args.panes[value].show()
|
||||
e.redraw()
|
||||
end
|
||||
end
|
||||
|
||||
e.set_value(1)
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return multipane
|
||||
|
||||
@@ -11,18 +11,24 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
---@class _pipe_map_entry
|
||||
---@field atr boolean align top right (or bottom left for false)
|
||||
---@field thin boolean thin pipe or not
|
||||
---@field fg string foreground blit
|
||||
---@field bg string background blit
|
||||
|
||||
-- new pipe network
|
||||
---@param args pipenet_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function pipenet(args)
|
||||
assert(type(args.pipes) == "table", "graphics.elements.indicators.pipenet: pipes is a required field")
|
||||
element.assert(type(args.pipes) == "table", "pipes is a required field")
|
||||
|
||||
args.width = 0
|
||||
args.height = 0
|
||||
|
||||
-- determine width/height
|
||||
for i = 1, #args.pipes do
|
||||
local pipe = args.pipes[i] ---@type pipe
|
||||
|
||||
@@ -43,105 +49,283 @@ local function pipenet(args)
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw all pipes
|
||||
-- determine if there are any thin pipes involved
|
||||
local any_thin = false
|
||||
for p = 1, #args.pipes do
|
||||
local pipe = args.pipes[p] ---@type pipe
|
||||
any_thin = args.pipes[p].thin
|
||||
if any_thin then break end
|
||||
end
|
||||
|
||||
local x = 1 + pipe.x1
|
||||
local y = 1 + pipe.y1
|
||||
-- draw all pipes by drawing out lines
|
||||
local function vector_draw()
|
||||
for p = 1, #args.pipes do
|
||||
local pipe = args.pipes[p] ---@type pipe
|
||||
|
||||
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
|
||||
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
|
||||
local x = 1 + pipe.x1
|
||||
local y = 1 + pipe.y1
|
||||
|
||||
e.window.setCursorPos(x, y)
|
||||
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
|
||||
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
|
||||
|
||||
local c = core.cpair(pipe.color, e.fg_bg.bkg)
|
||||
|
||||
if pipe.align_tr then
|
||||
-- cross width then height
|
||||
for i = 1, pipe.w do
|
||||
if pipe.thin then
|
||||
if i == pipe.w then
|
||||
-- corner
|
||||
if y_step > 0 then
|
||||
e.window.blit("\x93", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8e", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.w and y_step > 0 then
|
||||
-- corner
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8f", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
x = x + x_step
|
||||
e.window.setCursorPos(x, y)
|
||||
if pipe.thin then
|
||||
x_step = util.trinary(pipe.x1 == pipe.x2, 0, x_step)
|
||||
y_step = util.trinary(pipe.y1 == pipe.y2, 0, y_step)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
x = x - x_step
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
for _ = 1, pipe.h - 1 do
|
||||
y = y + y_step
|
||||
e.window.setCursorPos(x, y)
|
||||
local c = core.cpair(pipe.color, e.fg_bg.bkg)
|
||||
|
||||
if pipe.thin then
|
||||
e.window.blit("\x95", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
if pipe.align_tr then
|
||||
-- cross width then height
|
||||
for i = 1, pipe.w do
|
||||
if pipe.thin then
|
||||
if i == pipe.w then
|
||||
-- corner
|
||||
if y_step > 0 then
|
||||
e.w_blit("\x93", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit("\x8e", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.w and y_step > 0 then
|
||||
-- corner
|
||||
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit("\x8f", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
end
|
||||
|
||||
x = x + x_step
|
||||
e.w_set_cur(x, y)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
x = x - x_step
|
||||
|
||||
for _ = 1, pipe.h - 1 do
|
||||
y = y + y_step
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
if pipe.thin then
|
||||
e.w_blit("\x95", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- cross height then width
|
||||
for i = 1, pipe.h do
|
||||
if pipe.thin then
|
||||
if i == pipe.h then
|
||||
-- corner
|
||||
if y_step < 0 then
|
||||
e.w_blit("\x97", c.blit_bkg, c.blit_fgd)
|
||||
elseif y_step > 0 then
|
||||
e.w_blit("\x8d", c.blit_fgd, c.blit_bkg)
|
||||
else
|
||||
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.w_blit("\x95", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.h and y_step < 0 then
|
||||
-- corner
|
||||
e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.w_blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
|
||||
y = y + y_step
|
||||
e.w_set_cur(x, y)
|
||||
end
|
||||
|
||||
-- back up one
|
||||
y = y - y_step
|
||||
|
||||
for _ = 1, pipe.w - 1 do
|
||||
x = x + x_step
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
if pipe.thin then
|
||||
e.w_blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
else
|
||||
e.w_blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- draw a particular map cell
|
||||
---@param map table 2D cell map
|
||||
---@param x integer x coord
|
||||
---@param y integer y coord
|
||||
local function draw_map_cell(map, x, y)
|
||||
local entry = map[x][y] ---@type _pipe_map_entry already confirmed not false
|
||||
local char
|
||||
local invert = false
|
||||
|
||||
local function check(cx, cy)
|
||||
return (map[cx] ~= nil) and (map[cx][cy] ~= nil) and (map[cx][cy] ~= false) and (map[cx][cy].fg == entry.fg)
|
||||
end
|
||||
|
||||
if entry.thin then
|
||||
if check(x - 1, y) then -- if left
|
||||
if check(x, y - 1) then -- if above
|
||||
if check(x + 1, y) then -- if right
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x91", "\x9d")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = util.trinary(entry.atr, "\x8e", "\x8d")
|
||||
end
|
||||
else -- not right
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x91", "\x95")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = util.trinary(entry.atr, "\x8e", "\x85")
|
||||
end
|
||||
end
|
||||
elseif check(x, y + 1) then-- not above, if below
|
||||
if check(x + 1, y) then -- if right
|
||||
char = util.trinary(entry.atr, "\x93", "\x9c")
|
||||
invert = entry.atr
|
||||
else -- not right
|
||||
char = util.trinary(entry.atr, "\x93", "\x94")
|
||||
invert = entry.atr
|
||||
end
|
||||
else -- not above, not below
|
||||
char = "\x8c"
|
||||
end
|
||||
elseif check(x + 1, y) then -- not left, if right
|
||||
if check(x, y - 1) then -- if above
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x95", "\x9d")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = util.trinary(entry.atr, "\x8a", "\x8d")
|
||||
end
|
||||
else -- not above
|
||||
if check(x, y + 1) then -- if below
|
||||
char = util.trinary(entry.atr, "\x97", "\x9c")
|
||||
invert = entry.atr
|
||||
else -- not below
|
||||
char = "\x8c"
|
||||
end
|
||||
end
|
||||
else -- not left, not right
|
||||
char = "\x95"
|
||||
invert = entry.atr
|
||||
end
|
||||
else
|
||||
-- cross height then width
|
||||
for i = 1, pipe.h do
|
||||
if pipe.thin then
|
||||
if i == pipe.h then
|
||||
-- corner
|
||||
if y_step < 0 then
|
||||
e.window.blit("\x97", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit("\x8d", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
e.window.blit("\x95", c.blit_fgd, c.blit_bkg)
|
||||
end
|
||||
else
|
||||
if i == pipe.h and y_step < 0 then
|
||||
-- corner
|
||||
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
else
|
||||
e.window.blit(" ", c.blit_bkg, c.blit_fgd)
|
||||
end
|
||||
if check(x, y - 1) then -- above
|
||||
-- not below and (if left or right)
|
||||
if (not check(x, y + 1)) and (check(x - 1, y) or check(x + 1, y)) then
|
||||
char = util.trinary(entry.atr, "\x8f", " ")
|
||||
invert = not entry.atr
|
||||
else -- not below w/ sides only
|
||||
char = " "
|
||||
invert = true
|
||||
end
|
||||
elseif check(x, y + 1) then -- not above, if below
|
||||
-- if left or right
|
||||
if (check(x - 1, y) or check(x + 1, y)) then
|
||||
char = "\x83"
|
||||
invert = true
|
||||
else -- not left or right
|
||||
char = " "
|
||||
invert = true
|
||||
end
|
||||
else -- not above, not below
|
||||
char = util.trinary(entry.atr, "\x8f", "\x83")
|
||||
invert = not entry.atr
|
||||
end
|
||||
end
|
||||
|
||||
e.w_set_cur(x, y)
|
||||
|
||||
if invert then
|
||||
e.w_blit(char, entry.bg, entry.fg)
|
||||
else
|
||||
e.w_blit(char, entry.fg, entry.bg)
|
||||
end
|
||||
end
|
||||
|
||||
-- draw all pipes by assembling and marking up a 2D map<br>
|
||||
-- this is an easy way to check adjacent blocks, which is required to properly draw thin pipes
|
||||
local function map_draw()
|
||||
local map = {}
|
||||
|
||||
for x = 1, args.width do
|
||||
table.insert(map, {})
|
||||
for _ = 1, args.height do table.insert(map[x], false) end
|
||||
end
|
||||
|
||||
-- build map
|
||||
for p = 1, #args.pipes do
|
||||
local pipe = args.pipes[p] ---@type pipe
|
||||
|
||||
local x = 1 + pipe.x1
|
||||
local y = 1 + pipe.y1
|
||||
|
||||
local x_step = util.trinary(pipe.x1 >= pipe.x2, -1, 1)
|
||||
local y_step = util.trinary(pipe.y1 >= pipe.y2, -1, 1)
|
||||
|
||||
local entry = { atr = pipe.align_tr, thin = pipe.thin, fg = colors.toBlit(pipe.color), bg = e.fg_bg.blit_bkg }
|
||||
|
||||
if pipe.align_tr then
|
||||
-- cross width then height
|
||||
for _ = 1, pipe.w do
|
||||
map[x][y] = entry
|
||||
x = x + x_step
|
||||
end
|
||||
|
||||
y = y + y_step
|
||||
e.window.setCursorPos(x, y)
|
||||
end
|
||||
x = x - x_step -- back up one
|
||||
|
||||
-- back up one
|
||||
y = y - y_step
|
||||
for _ = 1, pipe.h do
|
||||
map[x][y] = entry
|
||||
y = y + y_step
|
||||
end
|
||||
else
|
||||
-- cross height then width
|
||||
for _ = 1, pipe.h do
|
||||
map[x][y] = entry
|
||||
y = y + y_step
|
||||
end
|
||||
|
||||
for _ = 1, pipe.w - 1 do
|
||||
x = x + x_step
|
||||
e.window.setCursorPos(x, y)
|
||||
y = y - y_step -- back up one
|
||||
|
||||
if pipe.thin then
|
||||
e.window.blit("\x8c", c.blit_fgd, c.blit_bkg)
|
||||
else
|
||||
e.window.blit("\x83", c.blit_bkg, c.blit_fgd)
|
||||
for _ = 1, pipe.w do
|
||||
map[x][y] = entry
|
||||
x = x + x_step
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- render
|
||||
for x = 1, args.width do
|
||||
for y = 1, args.height do
|
||||
if map[x][y] ~= false then draw_map_cell(map, x, y) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- element redraw
|
||||
function e.redraw()
|
||||
if any_thin then map_draw() else vector_draw() end
|
||||
end
|
||||
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return pipenet
|
||||
|
||||
@@ -11,17 +11,18 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new rectangle
|
||||
---@param args rectangle_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function rectangle(args)
|
||||
assert(args.border ~= nil or args.thin ~= true, "graphics.elements.rectangle: thin requires border to be provided")
|
||||
element.assert(args.border ~= nil or args.thin ~= true, "thin requires border to be provided")
|
||||
|
||||
-- if thin, then width will always need to be 1
|
||||
if args.thin == true then
|
||||
@@ -30,76 +31,84 @@ local function rectangle(args)
|
||||
end
|
||||
|
||||
-- offset children
|
||||
local offset_x = 0
|
||||
local offset_y = 0
|
||||
if args.border ~= nil then
|
||||
args.offset_x = args.border.width
|
||||
args.offset_y = args.border.width
|
||||
offset_x = args.border.width
|
||||
offset_y = args.border.width
|
||||
|
||||
-- slightly different y offset if the border is set to even
|
||||
if args.border.even then
|
||||
local width_x2 = (2 * args.border.width)
|
||||
args.offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
|
||||
offset_y = math.floor(width_x2 / 3) + util.trinary(width_x2 % 3 > 0, 1, 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
local e = element.new(args, offset_x, offset_y)
|
||||
|
||||
-- create content window for child elements
|
||||
e.content_window = window.create(e.window, 1 + offset_x, 1 + offset_y, e.frame.w - (2 * offset_x), e.frame.h - (2 * offset_y))
|
||||
e.content_window.setBackgroundColor(e.fg_bg.bkg)
|
||||
e.content_window.setTextColor(e.fg_bg.fgd)
|
||||
e.content_window.clear()
|
||||
|
||||
-- draw bordered box if requested
|
||||
-- element constructor will have drawn basic colored rectangle regardless
|
||||
if args.border ~= nil then
|
||||
e.window.setCursorPos(1, 1)
|
||||
e.w_set_cur(1, 1)
|
||||
|
||||
local border_width = args.offset_x
|
||||
local border_height = args.offset_y
|
||||
local border_width = offset_x
|
||||
local border_height = offset_y
|
||||
local border_blit = colors.toBlit(args.border.color)
|
||||
local width_x2 = border_width * 2
|
||||
local inner_width = e.frame.w - width_x2
|
||||
|
||||
-- check dimensions
|
||||
assert(width_x2 <= e.frame.w, "graphics.elements.rectangle: border too thick for width")
|
||||
assert(width_x2 <= e.frame.h, "graphics.elements.rectangle: border too thick for height")
|
||||
element.assert(width_x2 <= e.frame.w, "border too thick for width")
|
||||
element.assert(width_x2 <= e.frame.h, "border too thick for height")
|
||||
|
||||
-- form the basic line strings and top/bottom blit strings
|
||||
local spaces = util.spaces(e.frame.w)
|
||||
local blit_fg = util.strrep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local blit_fg = string.rep(e.fg_bg.blit_fgd, e.frame.w)
|
||||
local blit_fg_sides = blit_fg
|
||||
local blit_bg_sides = ""
|
||||
local blit_bg_top_bot = util.strrep(border_blit, e.frame.w)
|
||||
local blit_bg_top_bot = string.rep(border_blit, e.frame.w)
|
||||
|
||||
-- partial bars
|
||||
local p_a, p_b, p_s
|
||||
if args.thin == true then
|
||||
if args.even_inner == true then
|
||||
p_a = "\x9c" .. util.strrep("\x8c", inner_width) .. "\x93"
|
||||
p_b = "\x8d" .. util.strrep("\x8c", inner_width) .. "\x8e"
|
||||
p_a = "\x9c" .. string.rep("\x8c", inner_width) .. "\x93"
|
||||
p_b = "\x8d" .. string.rep("\x8c", inner_width) .. "\x8e"
|
||||
else
|
||||
p_a = "\x97" .. util.strrep("\x83", inner_width) .. "\x94"
|
||||
p_b = "\x8a" .. util.strrep("\x8f", inner_width) .. "\x85"
|
||||
p_a = "\x97" .. string.rep("\x83", inner_width) .. "\x94"
|
||||
p_b = "\x8a" .. string.rep("\x8f", inner_width) .. "\x85"
|
||||
end
|
||||
|
||||
p_s = "\x95" .. util.spaces(inner_width) .. "\x95"
|
||||
else
|
||||
if args.even_inner == true then
|
||||
p_a = util.strrep("\x83", inner_width + width_x2)
|
||||
p_b = util.strrep("\x8f", inner_width + width_x2)
|
||||
p_a = string.rep("\x83", inner_width + width_x2)
|
||||
p_b = string.rep("\x8f", inner_width + width_x2)
|
||||
else
|
||||
p_a = util.spaces(border_width) .. util.strrep("\x8f", inner_width) .. util.spaces(border_width)
|
||||
p_b = util.spaces(border_width) .. util.strrep("\x83", inner_width) .. util.spaces(border_width)
|
||||
p_a = util.spaces(border_width) .. string.rep("\x8f", inner_width) .. util.spaces(border_width)
|
||||
p_b = util.spaces(border_width) .. string.rep("\x83", inner_width) .. util.spaces(border_width)
|
||||
end
|
||||
|
||||
p_s = spaces
|
||||
end
|
||||
|
||||
local p_inv_fg = util.strrep(border_blit, border_width) .. util.strrep(e.fg_bg.blit_bkg, inner_width) ..
|
||||
util.strrep(border_blit, border_width)
|
||||
local p_inv_bg = util.strrep(e.fg_bg.blit_bkg, border_width) .. util.strrep(border_blit, inner_width) ..
|
||||
util.strrep(e.fg_bg.blit_bkg, border_width)
|
||||
local p_inv_fg = string.rep(border_blit, border_width) .. string.rep(e.fg_bg.blit_bkg, inner_width) ..
|
||||
string.rep(border_blit, border_width)
|
||||
local p_inv_bg = string.rep(e.fg_bg.blit_bkg, border_width) .. string.rep(border_blit, inner_width) ..
|
||||
string.rep(e.fg_bg.blit_bkg, border_width)
|
||||
|
||||
if args.thin == true then
|
||||
p_inv_fg = e.fg_bg.blit_bkg .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. util.strrep(border_blit, border_width)
|
||||
p_inv_bg = border_blit .. util.strrep(border_blit, inner_width) .. util.strrep(e.fg_bg.blit_bkg, border_width)
|
||||
p_inv_fg = e.fg_bg.blit_bkg .. string.rep(e.fg_bg.blit_bkg, inner_width) .. string.rep(border_blit, border_width)
|
||||
p_inv_bg = border_blit .. string.rep(border_blit, inner_width) .. string.rep(e.fg_bg.blit_bkg, border_width)
|
||||
|
||||
blit_fg_sides = border_blit .. util.strrep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg
|
||||
blit_fg_sides = border_blit .. string.rep(e.fg_bg.blit_bkg, inner_width) .. e.fg_bg.blit_bkg
|
||||
end
|
||||
|
||||
-- form the body blit strings (sides are border, inside is normal)
|
||||
@@ -117,67 +126,72 @@ local function rectangle(args)
|
||||
end
|
||||
|
||||
-- draw rectangle with borders
|
||||
for y = 1, e.frame.h do
|
||||
e.window.setCursorPos(1, y)
|
||||
-- top border
|
||||
if y <= border_height then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == border_height then
|
||||
if args.thin == true then
|
||||
e.window.blit(p_a, p_inv_bg, p_inv_fg)
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
|
||||
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
function e.redraw()
|
||||
for y = 1, e.frame.h do
|
||||
e.w_set_cur(1, y)
|
||||
-- top border
|
||||
if y <= border_height then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == border_height then
|
||||
if args.thin == true then
|
||||
e.w_blit(p_a, p_inv_bg, p_inv_fg)
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), p_inv_bg)
|
||||
local _bg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.window.blit(p_b, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.window.blit(p_a, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
else
|
||||
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
-- bottom border
|
||||
elseif y > (e.frame.h - border_width) then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == ((e.frame.h - border_width) + 1) then
|
||||
if args.thin == true then
|
||||
if args.even_inner == true then
|
||||
e.window.blit(p_b, blit_bg_top_bot, util.strrep(e.fg_bg.blit_bkg, e.frame.w))
|
||||
else
|
||||
e.window.blit(p_b, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
if width_x2 % 3 == 1 then
|
||||
e.w_blit(p_b, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.w_blit(p_a, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.w_blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
else
|
||||
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
local _bg = util.trinary(args.even_inner == true, util.strrep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.window.blit(p_a, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.window.blit(p_b, _fg, _bg)
|
||||
e.w_blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
-- bottom border
|
||||
elseif y > (e.frame.h - border_width) then
|
||||
-- partial pixel fill
|
||||
if args.border.even and y == ((e.frame.h - border_width) + 1) then
|
||||
if args.thin == true then
|
||||
if args.even_inner == true then
|
||||
e.w_blit(p_b, blit_bg_top_bot, string.rep(e.fg_bg.blit_bkg, e.frame.w))
|
||||
else
|
||||
e.w_blit(p_b, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
-- skip line
|
||||
e.window.blit(spaces, blit_fg, blit_bg_sides)
|
||||
local _fg = util.trinary(args.even_inner == true, blit_bg_top_bot, p_inv_fg)
|
||||
local _bg = util.trinary(args.even_inner == true, string.rep(e.fg_bg.blit_bkg, e.frame.w), blit_bg_top_bot)
|
||||
|
||||
if width_x2 % 3 == 1 then
|
||||
e.w_blit(p_a, _fg, _bg)
|
||||
elseif width_x2 % 3 == 2 then
|
||||
e.w_blit(p_b, _fg, _bg)
|
||||
else
|
||||
-- skip line
|
||||
e.w_blit(spaces, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
else
|
||||
e.w_blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
|
||||
end
|
||||
else
|
||||
if args.thin == true then
|
||||
e.window.blit(p_s, blit_fg_sides, blit_bg_sides)
|
||||
else
|
||||
e.window.blit(p_s, blit_fg, blit_bg_sides)
|
||||
if args.thin == true then
|
||||
e.w_blit(p_s, blit_fg_sides, blit_bg_sides)
|
||||
else
|
||||
e.w_blit(p_s, blit_fg, blit_bg_sides)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- initial draw of border
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
return e.get()
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return rectangle
|
||||
|
||||
@@ -5,37 +5,39 @@ local util = require("scada-common.util")
|
||||
local core = require("graphics.core")
|
||||
local element = require("graphics.element")
|
||||
|
||||
local TEXT_ALIGN = core.TEXT_ALIGN
|
||||
local ALIGN = core.ALIGN
|
||||
|
||||
---@class textbox_args
|
||||
---@field text string text to show
|
||||
---@field alignment? TEXT_ALIGN text alignment, left by default
|
||||
---@field alignment? ALIGN text alignment, left by default
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new text box
|
||||
---@param args textbox_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function textbox(args)
|
||||
assert(type(args.text) == "string", "graphics.elements.textbox: text is a required field")
|
||||
element.assert(type(args.text) == "string", "text is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
local alignment = args.alignment or TEXT_ALIGN.LEFT
|
||||
e.value = args.text
|
||||
|
||||
local alignment = args.alignment or ALIGN.LEFT
|
||||
|
||||
-- draw textbox
|
||||
function e.redraw()
|
||||
e.window.clear()
|
||||
|
||||
local function display_text(text)
|
||||
e.value = text
|
||||
|
||||
local lines = util.strwrap(text, e.frame.w)
|
||||
local lines = util.strwrap(e.value, e.frame.w)
|
||||
|
||||
for i = 1, #lines do
|
||||
if i > e.frame.h then break end
|
||||
@@ -43,28 +45,29 @@ local function textbox(args)
|
||||
local len = string.len(lines[i])
|
||||
|
||||
-- use cursor position to align this line
|
||||
if alignment == TEXT_ALIGN.CENTER then
|
||||
e.window.setCursorPos(math.floor((e.frame.w - len) / 2) + 1, i)
|
||||
elseif alignment == TEXT_ALIGN.RIGHT then
|
||||
e.window.setCursorPos((e.frame.w - len) + 1, i)
|
||||
if alignment == ALIGN.CENTER then
|
||||
e.w_set_cur(math.floor((e.frame.w - len) / 2) + 1, i)
|
||||
elseif alignment == ALIGN.RIGHT then
|
||||
e.w_set_cur((e.frame.w - len) + 1, i)
|
||||
else
|
||||
e.window.setCursorPos(1, i)
|
||||
e.w_set_cur(1, i)
|
||||
end
|
||||
|
||||
e.window.write(lines[i])
|
||||
e.w_write(lines[i])
|
||||
end
|
||||
end
|
||||
|
||||
display_text(args.text)
|
||||
|
||||
-- set the string value and re-draw the text
|
||||
---@param val string value
|
||||
function e.set_value(val)
|
||||
e.window.clear()
|
||||
display_text(val)
|
||||
e.value = val
|
||||
e.redraw()
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return textbox
|
||||
|
||||
@@ -11,23 +11,22 @@ local element = require("graphics.element")
|
||||
---@field parent graphics_element
|
||||
---@field id? string element id
|
||||
---@field x? integer 1 if omitted
|
||||
---@field y? integer 1 if omitted
|
||||
---@field y? integer auto incremented if omitted
|
||||
---@field width? integer parent width if omitted
|
||||
---@field height? integer parent height if omitted
|
||||
---@field gframe? graphics_frame frame instead of x/y/width/height
|
||||
---@field fg_bg? cpair foreground/background colors
|
||||
---@field hidden? boolean true to hide on initial draw
|
||||
|
||||
-- new tiling box
|
||||
---@param args tiling_args
|
||||
---@return graphics_element element, element_id id
|
||||
local function tiling(args)
|
||||
assert(type(args.fill_c) == "table", "graphics.elements.tiling: fill_c is a required field")
|
||||
element.assert(type(args.fill_c) == "table", "fill_c is a required field")
|
||||
|
||||
-- create new graphics element base object
|
||||
local e = element.new(args)
|
||||
|
||||
-- draw tiling box
|
||||
|
||||
local fill_a = args.fill_c.blit_a
|
||||
local fill_b = args.fill_c.blit_b
|
||||
|
||||
@@ -37,13 +36,9 @@ local function tiling(args)
|
||||
local start_y = 1
|
||||
local inner_width = math.floor(e.frame.w / util.trinary(even, 2, 1))
|
||||
local inner_height = e.frame.h
|
||||
local alternator = true
|
||||
|
||||
-- border
|
||||
if args.border_c ~= nil then
|
||||
e.window.setBackgroundColor(args.border_c)
|
||||
e.window.clear()
|
||||
|
||||
start_x = 1 + util.trinary(even, 2, 1)
|
||||
start_y = 2
|
||||
|
||||
@@ -52,36 +47,49 @@ local function tiling(args)
|
||||
end
|
||||
|
||||
-- check dimensions
|
||||
assert(inner_width > 0, "graphics.elements.tiling: inner_width <= 0")
|
||||
assert(inner_height > 0, "graphics.elements.tiling: inner_height <= 0")
|
||||
assert(start_x <= inner_width, "graphics.elements.tiling: start_x > inner_width")
|
||||
assert(start_y <= inner_height, "graphics.elements.tiling: start_y > inner_height")
|
||||
element.assert(inner_width > 0, "inner_width <= 0")
|
||||
element.assert(inner_height > 0, "inner_height <= 0")
|
||||
element.assert(start_x <= inner_width, "start_x > inner_width")
|
||||
element.assert(start_y <= inner_height, "start_y > inner_height")
|
||||
|
||||
-- create pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.window.setCursorPos(start_x, y)
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
if even then
|
||||
e.window.blit(" ", "00", fill_a .. fill_a)
|
||||
else
|
||||
e.window.blit(" ", "0", fill_a)
|
||||
end
|
||||
else
|
||||
if even then
|
||||
e.window.blit(" ", "00", fill_b .. fill_b)
|
||||
else
|
||||
e.window.blit(" ", "0", fill_b)
|
||||
end
|
||||
end
|
||||
-- draw tiling box
|
||||
function e.redraw()
|
||||
local alternator = true
|
||||
|
||||
alternator = not alternator
|
||||
if args.border_c ~= nil then
|
||||
e.w_set_bkg(args.border_c)
|
||||
e.window.clear()
|
||||
end
|
||||
|
||||
if inner_width % 2 == 0 then alternator = not alternator end
|
||||
-- draw pattern
|
||||
for y = start_y, inner_height + (start_y - 1) do
|
||||
e.w_set_cur(start_x, y)
|
||||
for _ = 1, inner_width do
|
||||
if alternator then
|
||||
if even then
|
||||
e.w_blit(" ", "00", fill_a .. fill_a)
|
||||
else
|
||||
e.w_blit(" ", "0", fill_a)
|
||||
end
|
||||
else
|
||||
if even then
|
||||
e.w_blit(" ", "00", fill_b .. fill_b)
|
||||
else
|
||||
e.w_blit(" ", "0", fill_b)
|
||||
end
|
||||
end
|
||||
|
||||
alternator = not alternator
|
||||
end
|
||||
|
||||
if inner_width % 2 == 0 then alternator = not alternator end
|
||||
end
|
||||
end
|
||||
|
||||
return e.get()
|
||||
-- initial draw
|
||||
e.redraw()
|
||||
|
||||
return e.complete()
|
||||
end
|
||||
|
||||
return tiling
|
||||
|
||||
@@ -4,46 +4,77 @@
|
||||
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local DOUBLE_CLICK_MS = 500
|
||||
|
||||
local events = {}
|
||||
|
||||
---@enum CLICK_BUTTON
|
||||
events.CLICK_BUTTON = {
|
||||
local CLICK_BUTTON = {
|
||||
GENERIC = 0,
|
||||
LEFT_BUTTON = 1,
|
||||
RIGHT_BUTTON = 2,
|
||||
MID_BUTTON = 3
|
||||
}
|
||||
|
||||
---@enum CLICK_TYPE
|
||||
events.CLICK_TYPE = {
|
||||
events.CLICK_BUTTON = CLICK_BUTTON
|
||||
|
||||
---@enum MOUSE_CLICK
|
||||
local MOUSE_CLICK = {
|
||||
TAP = 1, -- screen tap (complete click)
|
||||
DOWN = 2, -- button down
|
||||
UP = 3, -- button up (completed a click)
|
||||
DRAG = 4, -- mouse dragged
|
||||
SCROLL_DOWN = 5, -- scroll down
|
||||
SCROLL_UP = 6 -- scroll up
|
||||
SCROLL_UP = 6, -- scroll up
|
||||
DOUBLE_CLICK = 7 -- double left click
|
||||
}
|
||||
|
||||
events.MOUSE_CLICK = MOUSE_CLICK
|
||||
|
||||
---@enum KEY_CLICK
|
||||
local KEY_CLICK = {
|
||||
DOWN = 1,
|
||||
HELD = 2,
|
||||
UP = 3,
|
||||
CHAR = 4
|
||||
}
|
||||
|
||||
events.KEY_CLICK = KEY_CLICK
|
||||
|
||||
-- create a new 2D coordinate
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return coordinate_2d
|
||||
local function _coord2d(x, y) return { x = x, y = y } end
|
||||
|
||||
events.new_coord_2d = _coord2d
|
||||
|
||||
---@class mouse_interaction
|
||||
---@field monitor string
|
||||
---@field button CLICK_BUTTON
|
||||
---@field type CLICK_TYPE
|
||||
---@field type MOUSE_CLICK
|
||||
---@field initial coordinate_2d
|
||||
---@field current coordinate_2d
|
||||
|
||||
---@class key_interaction
|
||||
---@field type KEY_CLICK
|
||||
---@field key number key code
|
||||
---@field name string key character name
|
||||
---@field shift boolean shift held
|
||||
---@field ctrl boolean ctrl held
|
||||
---@field alt boolean alt held
|
||||
|
||||
local handler = {
|
||||
-- left, right, middle button down tracking
|
||||
button_down = {
|
||||
_coord2d(0, 0),
|
||||
_coord2d(0, 0),
|
||||
_coord2d(0, 0)
|
||||
}
|
||||
button_down = { _coord2d(0, 0), _coord2d(0, 0), _coord2d(0, 0) },
|
||||
-- keyboard modifiers
|
||||
shift = false,
|
||||
alt = false,
|
||||
ctrl = false,
|
||||
-- double click tracking
|
||||
dc_start = 0,
|
||||
dc_step = 1,
|
||||
dc_coord = _coord2d(0, 0)
|
||||
}
|
||||
|
||||
-- create a new monitor touch mouse interaction event
|
||||
@@ -55,8 +86,8 @@ local handler = {
|
||||
local function _monitor_touch(monitor, x, y)
|
||||
return {
|
||||
monitor = monitor,
|
||||
button = events.CLICK_BUTTON.GENERIC,
|
||||
type = events.CLICK_TYPE.TAP,
|
||||
button = CLICK_BUTTON.GENERIC,
|
||||
type = MOUSE_CLICK.TAP,
|
||||
initial = _coord2d(x, y),
|
||||
current = _coord2d(x, y)
|
||||
}
|
||||
@@ -65,7 +96,7 @@ end
|
||||
-- create a new mouse button mouse interaction event
|
||||
---@nodiscard
|
||||
---@param button CLICK_BUTTON mouse button
|
||||
---@param type CLICK_TYPE click type
|
||||
---@param type MOUSE_CLICK click type
|
||||
---@param x1 integer initial x
|
||||
---@param y1 integer initial y
|
||||
---@param x2 integer current x
|
||||
@@ -83,14 +114,14 @@ end
|
||||
|
||||
-- create a new generic mouse interaction event
|
||||
---@nodiscard
|
||||
---@param type CLICK_TYPE
|
||||
---@param type MOUSE_CLICK
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return mouse_interaction
|
||||
function events.mouse_generic(type, x, y)
|
||||
return {
|
||||
monitor = "",
|
||||
button = events.CLICK_BUTTON.GENERIC,
|
||||
button = CLICK_BUTTON.GENERIC,
|
||||
type = type,
|
||||
initial = _coord2d(x, y),
|
||||
current = _coord2d(x, y)
|
||||
@@ -115,8 +146,8 @@ end
|
||||
|
||||
-- check if an event qualifies as a click (tap or up)
|
||||
---@nodiscard
|
||||
---@param t CLICK_TYPE
|
||||
function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.CLICK_TYPE.UP end
|
||||
---@param t MOUSE_CLICK
|
||||
function events.was_clicked(t) return t == MOUSE_CLICK.TAP or t == MOUSE_CLICK.UP end
|
||||
|
||||
-- create a new mouse event to pass onto graphics renderer<br>
|
||||
-- supports: mouse_click, mouse_up, mouse_drag, mouse_scroll, and monitor_touch
|
||||
@@ -126,35 +157,97 @@ function events.was_clicked(t) return t == events.CLICK_TYPE.TAP or t == events.
|
||||
---@param y integer y coordinate
|
||||
---@return mouse_interaction|nil
|
||||
function events.new_mouse_event(event_type, opt, x, y)
|
||||
local h = handler
|
||||
|
||||
if event_type == "mouse_click" then
|
||||
---@cast opt 1|2|3
|
||||
handler.button_down[opt] = _coord2d(x, y)
|
||||
return _mouse_event(opt, events.CLICK_TYPE.DOWN, x, y, x, y)
|
||||
|
||||
local init = true
|
||||
|
||||
if opt == 1 and (h.dc_step % 2) == 1 then
|
||||
if h.dc_step ~= 1 and h.dc_coord.x == x and h.dc_coord.y == y and (util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
|
||||
init = false
|
||||
h.dc_step = h.dc_step + 1
|
||||
end
|
||||
end
|
||||
|
||||
if init then
|
||||
h.dc_start = util.time_ms()
|
||||
h.dc_coord = _coord2d(x, y)
|
||||
h.dc_step = 2
|
||||
end
|
||||
|
||||
h.button_down[opt] = _coord2d(x, y)
|
||||
return _mouse_event(opt, MOUSE_CLICK.DOWN, x, y, x, y)
|
||||
elseif event_type == "mouse_up" then
|
||||
---@cast opt 1|2|3
|
||||
local initial = handler.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, events.CLICK_TYPE.UP, initial.x, initial.y, x, y)
|
||||
|
||||
if opt == 1 and (h.dc_step % 2) == 0 and h.dc_coord.x == x and h.dc_coord.y == y and
|
||||
(util.time_ms() - h.dc_start) < DOUBLE_CLICK_MS then
|
||||
if h.dc_step == 4 then
|
||||
util.push_event("double_click", 1, x, y)
|
||||
h.dc_step = 1
|
||||
else h.dc_step = h.dc_step + 1 end
|
||||
else h.dc_step = 1 end
|
||||
|
||||
local initial = h.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, MOUSE_CLICK.UP, initial.x, initial.y, x, y)
|
||||
elseif event_type == "monitor_touch" then
|
||||
---@cast opt string
|
||||
return _monitor_touch(opt, x, y)
|
||||
elseif event_type == "mouse_drag" then
|
||||
---@cast opt 1|2|3
|
||||
local initial = handler.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, events.CLICK_TYPE.DRAG, initial.x, initial.y, x, y)
|
||||
local initial = h.button_down[opt] ---@type coordinate_2d
|
||||
return _mouse_event(opt, MOUSE_CLICK.DRAG, initial.x, initial.y, x, y)
|
||||
elseif event_type == "mouse_scroll" then
|
||||
---@cast opt 1|-1
|
||||
local scroll_direction = util.trinary(opt == 1, events.CLICK_TYPE.SCROLL_DOWN, events.CLICK_TYPE.SCROLL_UP)
|
||||
return _mouse_event(events.CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
|
||||
local scroll_direction = util.trinary(opt == 1, MOUSE_CLICK.SCROLL_DOWN, MOUSE_CLICK.SCROLL_UP)
|
||||
return _mouse_event(CLICK_BUTTON.GENERIC, scroll_direction, x, y, x, y)
|
||||
elseif event_type == "double_click" then
|
||||
return _mouse_event(CLICK_BUTTON.LEFT_BUTTON, MOUSE_CLICK.DOUBLE_CLICK, x, y, x, y)
|
||||
end
|
||||
end
|
||||
|
||||
-- create a new key event to pass onto graphics renderer<br>
|
||||
-- create a new keyboard interaction event
|
||||
---@nodiscard
|
||||
---@param click_type KEY_CLICK key click type
|
||||
---@param key integer|string keyboard key code or character for 'char' event
|
||||
---@return key_interaction
|
||||
local function _key_event(click_type, key)
|
||||
local name = key
|
||||
if type(key) == "number" then name = keys.getName(key) end
|
||||
return { type = click_type, key = key, name = name, shift = handler.shift, ctrl = handler.ctrl, alt = handler.alt }
|
||||
end
|
||||
|
||||
-- create a new keyboard event to pass onto graphics renderer<br>
|
||||
-- supports: char, key, and key_up
|
||||
---@param event_type os_event
|
||||
function events.new_key_event(event_type)
|
||||
---@param event_type os_event OS event to handle
|
||||
---@param key integer keyboard key code
|
||||
---@param held boolean? if the key is being held (for 'key' event)
|
||||
---@return key_interaction|nil
|
||||
function events.new_key_event(event_type, key, held)
|
||||
if event_type == "char" then
|
||||
return _key_event(KEY_CLICK.CHAR, key)
|
||||
elseif event_type == "key" then
|
||||
if key == keys.leftShift or key == keys.rightShift then
|
||||
handler.shift = true
|
||||
elseif key == keys.leftCtrl or key == keys.rightCtrl then
|
||||
handler.ctrl = true
|
||||
elseif key == keys.leftAlt or key == keys.rightAlt then
|
||||
handler.alt = true
|
||||
else
|
||||
return _key_event(util.trinary(held, KEY_CLICK.HELD, KEY_CLICK.DOWN), key)
|
||||
end
|
||||
elseif event_type == "key_up" then
|
||||
if key == keys.leftShift or key == keys.rightShift then
|
||||
handler.shift = false
|
||||
elseif key == keys.leftCtrl or key == keys.rightCtrl then
|
||||
handler.ctrl = false
|
||||
elseif key == keys.leftAlt or key == keys.rightAlt then
|
||||
handler.alt = false
|
||||
else
|
||||
return _key_event(KEY_CLICK.UP, key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- Indicator Light Flasher
|
||||
--
|
||||
|
||||
local tcd = require("scada-common.tcallbackdsp")
|
||||
local tcd = require("scada-common.tcd")
|
||||
|
||||
local flasher = {}
|
||||
|
||||
@@ -43,8 +43,10 @@ end
|
||||
|
||||
-- start/resume the flasher periodic
|
||||
function flasher.run()
|
||||
active = true
|
||||
callback_250ms()
|
||||
if not active then
|
||||
active = true
|
||||
callback_250ms()
|
||||
end
|
||||
end
|
||||
|
||||
-- clear all blinking indicators and stop the flasher periodic
|
||||
|
||||
25
imgen.py
25
imgen.py
@@ -23,11 +23,11 @@ def dir_size(path):
|
||||
return total
|
||||
|
||||
# get the version of an application at the provided path
|
||||
def get_version(path, is_comms = False):
|
||||
def get_version(path, is_lib = False):
|
||||
ver = ""
|
||||
string = "comms.version = \""
|
||||
string = ".version = \""
|
||||
|
||||
if not is_comms:
|
||||
if not is_lib:
|
||||
string = "_VERSION = \""
|
||||
|
||||
f = open(path, "r")
|
||||
@@ -48,7 +48,10 @@ def make_manifest(size):
|
||||
"versions" : {
|
||||
"installer" : get_version("./ccmsi.lua"),
|
||||
"bootloader" : get_version("./startup.lua"),
|
||||
"common" : get_version("./scada-common/util.lua", True),
|
||||
"comms" : get_version("./scada-common/comms.lua", True),
|
||||
"graphics" : get_version("./graphics/core.lua", True),
|
||||
"lockbox" : get_version("./lockbox/init.lua", True),
|
||||
"reactor-plc" : get_version("./reactor-plc/startup.lua"),
|
||||
"rtu" : get_version("./rtu/startup.lua"),
|
||||
"supervisor" : get_version("./supervisor/startup.lua"),
|
||||
@@ -57,7 +60,7 @@ def make_manifest(size):
|
||||
},
|
||||
"files" : {
|
||||
# common files
|
||||
"system" : [ "initenv.lua", "startup.lua" ],
|
||||
"system" : [ "initenv.lua", "startup.lua", "configure.lua", "LICENSE" ],
|
||||
"common" : list_files("./scada-common"),
|
||||
"graphics" : list_files("./graphics"),
|
||||
"lockbox" : list_files("./lockbox"),
|
||||
@@ -69,17 +72,17 @@ def make_manifest(size):
|
||||
"pocket" : list_files("./pocket"),
|
||||
},
|
||||
"depends" : {
|
||||
"reactor-plc" : [ "system", "common", "graphics" ],
|
||||
"rtu" : [ "system", "common", "graphics" ],
|
||||
"supervisor" : [ "system", "common" ],
|
||||
"coordinator" : [ "system", "common", "graphics" ],
|
||||
"pocket" : [ "system", "common", "graphics" ]
|
||||
"reactor-plc" : [ "system", "common", "graphics", "lockbox" ],
|
||||
"rtu" : [ "system", "common", "graphics", "lockbox" ],
|
||||
"supervisor" : [ "system", "common", "graphics", "lockbox" ],
|
||||
"coordinator" : [ "system", "common", "graphics", "lockbox" ],
|
||||
"pocket" : [ "system", "common", "graphics", "lockbox" ]
|
||||
},
|
||||
"sizes" : {
|
||||
# manifest file estimate
|
||||
"manifest" : size,
|
||||
# common files
|
||||
"system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua"),
|
||||
"system" : os.path.getsize("initenv.lua") + os.path.getsize("startup.lua") + os.path.getsize("configure.lua"),
|
||||
"common" : dir_size("./scada-common"),
|
||||
"graphics" : dir_size("./graphics"),
|
||||
"lockbox" : dir_size("./lockbox"),
|
||||
@@ -111,7 +114,7 @@ f.close()
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "shields":
|
||||
# write all the JSON files for shields.io
|
||||
for key, version in final_manifest["versions"].items():
|
||||
f = open("./shields/" + key + ".json", "w")
|
||||
f = open("./deploy/" + key + ".json", "w")
|
||||
|
||||
if version.find("alpha") >= 0:
|
||||
color = "yellow"
|
||||
|
||||
22
initenv.lua
22
initenv.lua
@@ -1,18 +1,8 @@
|
||||
--
|
||||
-- Initialize the Post-Boot Module Environment
|
||||
--
|
||||
|
||||
return {
|
||||
-- initialize booted environment
|
||||
init_env = function ()
|
||||
local _require = require("cc.require")
|
||||
local _env = setmetatable({}, { __index = _ENV })
|
||||
|
||||
-- overwrite require/package globals
|
||||
require, package = _require.make(_env, "/")
|
||||
|
||||
-- reset terminal
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
-- initialize booted environment
|
||||
init_env = function ()
|
||||
local _require, _env = require("cc.require"), setmetatable({}, { __index = _ENV })
|
||||
require, package = _require.make(_env, "/")
|
||||
term.clear(); term.setCursorPos(1, 1)
|
||||
end
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,415 +0,0 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Bit = require("lockbox.util.bit");
|
||||
|
||||
local XOR = Bit.bxor;
|
||||
|
||||
local SBOX = {
|
||||
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
|
||||
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
|
||||
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
|
||||
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
|
||||
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
|
||||
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
|
||||
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
|
||||
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
|
||||
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
|
||||
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
|
||||
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
|
||||
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
|
||||
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
|
||||
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
|
||||
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
|
||||
|
||||
local ISBOX = {
|
||||
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
|
||||
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
|
||||
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
|
||||
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
|
||||
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
|
||||
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
|
||||
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
|
||||
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
|
||||
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
|
||||
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
|
||||
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
|
||||
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
|
||||
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
|
||||
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
|
||||
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
|
||||
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
|
||||
|
||||
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
|
||||
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
|
||||
|
||||
local ETABLE = {
|
||||
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
|
||||
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
|
||||
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
|
||||
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
|
||||
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
|
||||
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
|
||||
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
|
||||
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
|
||||
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
|
||||
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
|
||||
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
|
||||
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
|
||||
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
|
||||
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
|
||||
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
|
||||
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
|
||||
|
||||
local LTABLE = {
|
||||
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
|
||||
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
|
||||
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
|
||||
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
|
||||
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
|
||||
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
|
||||
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
|
||||
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
|
||||
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
|
||||
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
|
||||
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
|
||||
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
|
||||
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
|
||||
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
|
||||
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
|
||||
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
|
||||
|
||||
local MIXTABLE = {
|
||||
0x02, 0x03, 0x01, 0x01,
|
||||
0x01, 0x02, 0x03, 0x01,
|
||||
0x01, 0x01, 0x02, 0x03,
|
||||
0x03, 0x01, 0x01, 0x02};
|
||||
|
||||
local IMIXTABLE = {
|
||||
0x0E, 0x0B, 0x0D, 0x09,
|
||||
0x09, 0x0E, 0x0B, 0x0D,
|
||||
0x0D, 0x09, 0x0E, 0x0B,
|
||||
0x0B, 0x0D, 0x09, 0x0E};
|
||||
|
||||
local RCON = {
|
||||
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
|
||||
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
|
||||
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
|
||||
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
|
||||
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
|
||||
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
|
||||
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
|
||||
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
|
||||
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
|
||||
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
|
||||
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
|
||||
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
|
||||
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
|
||||
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
|
||||
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
|
||||
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
|
||||
|
||||
|
||||
local GMUL = function(A, B)
|
||||
if(A == 0x01) then return B; end
|
||||
if(B == 0x01) then return A; end
|
||||
if(A == 0x00) then return 0; end
|
||||
if(B == 0x00) then return 0; end
|
||||
|
||||
local LA = LTABLE[A];
|
||||
local LB = LTABLE[B];
|
||||
|
||||
local sum = LA + LB;
|
||||
if (sum > 0xFF) then sum = sum - 0xFF; end
|
||||
|
||||
return ETABLE[sum];
|
||||
end
|
||||
|
||||
local byteSub = Array.substitute;
|
||||
|
||||
local shiftRow = Array.permute;
|
||||
|
||||
local mixCol = function(i, mix)
|
||||
local out = {};
|
||||
|
||||
local a, b, c, d;
|
||||
|
||||
a = GMUL(i[ 1], mix[ 1]);
|
||||
b = GMUL(i[ 2], mix[ 2]);
|
||||
c = GMUL(i[ 3], mix[ 3]);
|
||||
d = GMUL(i[ 4], mix[ 4]);
|
||||
out[ 1] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 5]);
|
||||
b = GMUL(i[ 2], mix[ 6]);
|
||||
c = GMUL(i[ 3], mix[ 7]);
|
||||
d = GMUL(i[ 4], mix[ 8]);
|
||||
out[ 2] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 9]);
|
||||
b = GMUL(i[ 2], mix[10]);
|
||||
c = GMUL(i[ 3], mix[11]);
|
||||
d = GMUL(i[ 4], mix[12]);
|
||||
out[ 3] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[13]);
|
||||
b = GMUL(i[ 2], mix[14]);
|
||||
c = GMUL(i[ 3], mix[15]);
|
||||
d = GMUL(i[ 4], mix[16]);
|
||||
out[ 4] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 5], mix[ 1]);
|
||||
b = GMUL(i[ 6], mix[ 2]);
|
||||
c = GMUL(i[ 7], mix[ 3]);
|
||||
d = GMUL(i[ 8], mix[ 4]);
|
||||
out[ 5] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 5]);
|
||||
b = GMUL(i[ 6], mix[ 6]);
|
||||
c = GMUL(i[ 7], mix[ 7]);
|
||||
d = GMUL(i[ 8], mix[ 8]);
|
||||
out[ 6] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 9]);
|
||||
b = GMUL(i[ 6], mix[10]);
|
||||
c = GMUL(i[ 7], mix[11]);
|
||||
d = GMUL(i[ 8], mix[12]);
|
||||
out[ 7] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[13]);
|
||||
b = GMUL(i[ 6], mix[14]);
|
||||
c = GMUL(i[ 7], mix[15]);
|
||||
d = GMUL(i[ 8], mix[16]);
|
||||
out[ 8] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 9], mix[ 1]);
|
||||
b = GMUL(i[10], mix[ 2]);
|
||||
c = GMUL(i[11], mix[ 3]);
|
||||
d = GMUL(i[12], mix[ 4]);
|
||||
out[ 9] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 5]);
|
||||
b = GMUL(i[10], mix[ 6]);
|
||||
c = GMUL(i[11], mix[ 7]);
|
||||
d = GMUL(i[12], mix[ 8]);
|
||||
out[10] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 9]);
|
||||
b = GMUL(i[10], mix[10]);
|
||||
c = GMUL(i[11], mix[11]);
|
||||
d = GMUL(i[12], mix[12]);
|
||||
out[11] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[13]);
|
||||
b = GMUL(i[10], mix[14]);
|
||||
c = GMUL(i[11], mix[15]);
|
||||
d = GMUL(i[12], mix[16]);
|
||||
out[12] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[13], mix[ 1]);
|
||||
b = GMUL(i[14], mix[ 2]);
|
||||
c = GMUL(i[15], mix[ 3]);
|
||||
d = GMUL(i[16], mix[ 4]);
|
||||
out[13] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 5]);
|
||||
b = GMUL(i[14], mix[ 6]);
|
||||
c = GMUL(i[15], mix[ 7]);
|
||||
d = GMUL(i[16], mix[ 8]);
|
||||
out[14] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 9]);
|
||||
b = GMUL(i[14], mix[10]);
|
||||
c = GMUL(i[15], mix[11]);
|
||||
d = GMUL(i[16], mix[12]);
|
||||
out[15] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[13]);
|
||||
b = GMUL(i[14], mix[14]);
|
||||
c = GMUL(i[15], mix[15]);
|
||||
d = GMUL(i[16], mix[16]);
|
||||
out[16] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyRound = function(key, round)
|
||||
local out = {};
|
||||
|
||||
out[ 1] = XOR(key[ 1], XOR(SBOX[key[14]], RCON[round]));
|
||||
out[ 2] = XOR(key[ 2], SBOX[key[15]]);
|
||||
out[ 3] = XOR(key[ 3], SBOX[key[16]]);
|
||||
out[ 4] = XOR(key[ 4], SBOX[key[13]]);
|
||||
|
||||
out[ 5] = XOR(out[ 1], key[ 5]);
|
||||
out[ 6] = XOR(out[ 2], key[ 6]);
|
||||
out[ 7] = XOR(out[ 3], key[ 7]);
|
||||
out[ 8] = XOR(out[ 4], key[ 8]);
|
||||
|
||||
out[ 9] = XOR(out[ 5], key[ 9]);
|
||||
out[10] = XOR(out[ 6], key[10]);
|
||||
out[11] = XOR(out[ 7], key[11]);
|
||||
out[12] = XOR(out[ 8], key[12]);
|
||||
|
||||
out[13] = XOR(out[ 9], key[13]);
|
||||
out[14] = XOR(out[10], key[14]);
|
||||
out[15] = XOR(out[11], key[15]);
|
||||
out[16] = XOR(out[12], key[16]);
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyExpand = function(key)
|
||||
local keys = {};
|
||||
|
||||
local temp = key;
|
||||
|
||||
keys[1] = temp;
|
||||
|
||||
for i = 1, 10 do
|
||||
temp = keyRound(temp, i);
|
||||
keys[i + 1] = temp;
|
||||
end
|
||||
|
||||
return keys;
|
||||
|
||||
end
|
||||
|
||||
local addKey = Array.XOR;
|
||||
|
||||
|
||||
|
||||
local AES = {};
|
||||
|
||||
AES.blockSize = 16;
|
||||
|
||||
AES.encrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
--round 1
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[2]);
|
||||
|
||||
--round 2
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[3]);
|
||||
|
||||
--round 3
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[4]);
|
||||
|
||||
--round 4
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[5]);
|
||||
|
||||
--round 5
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[6]);
|
||||
|
||||
--round 6
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[7]);
|
||||
|
||||
--round 7
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[8]);
|
||||
|
||||
--round 8
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[9]);
|
||||
|
||||
--round 9
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[10]);
|
||||
|
||||
--round 10
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = addKey(block, key[11]);
|
||||
|
||||
return block;
|
||||
|
||||
end
|
||||
|
||||
AES.decrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[11]);
|
||||
|
||||
--round 1
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[10]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 2
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[9]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 3
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[8]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 4
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[7]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 5
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[6]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 6
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[5]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 7
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[4]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 8
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[3]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 9
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[2]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 10
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
return block;
|
||||
end
|
||||
|
||||
return AES;
|
||||
@@ -1,462 +0,0 @@
|
||||
|
||||
local Array = require("lockbox.util.array");
|
||||
local Bit = require("lockbox.util.bit");
|
||||
|
||||
local XOR = Bit.bxor;
|
||||
|
||||
local SBOX = {
|
||||
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
|
||||
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
|
||||
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
|
||||
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
|
||||
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
|
||||
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
|
||||
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
|
||||
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
|
||||
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
|
||||
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
|
||||
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
|
||||
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
|
||||
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
|
||||
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
|
||||
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
|
||||
|
||||
local ISBOX = {
|
||||
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
|
||||
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
|
||||
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
|
||||
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
|
||||
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
|
||||
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
|
||||
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
|
||||
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
|
||||
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
|
||||
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
|
||||
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
|
||||
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
|
||||
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
|
||||
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
|
||||
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
|
||||
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
|
||||
|
||||
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
|
||||
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
|
||||
|
||||
local ETABLE = {
|
||||
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
|
||||
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
|
||||
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
|
||||
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
|
||||
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
|
||||
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
|
||||
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
|
||||
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
|
||||
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
|
||||
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
|
||||
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
|
||||
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
|
||||
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
|
||||
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
|
||||
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
|
||||
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
|
||||
|
||||
local LTABLE = {
|
||||
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
|
||||
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
|
||||
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
|
||||
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
|
||||
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
|
||||
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
|
||||
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
|
||||
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
|
||||
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
|
||||
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
|
||||
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
|
||||
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
|
||||
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
|
||||
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
|
||||
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
|
||||
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
|
||||
|
||||
local MIXTABLE = {
|
||||
0x02, 0x03, 0x01, 0x01,
|
||||
0x01, 0x02, 0x03, 0x01,
|
||||
0x01, 0x01, 0x02, 0x03,
|
||||
0x03, 0x01, 0x01, 0x02};
|
||||
|
||||
local IMIXTABLE = {
|
||||
0x0E, 0x0B, 0x0D, 0x09,
|
||||
0x09, 0x0E, 0x0B, 0x0D,
|
||||
0x0D, 0x09, 0x0E, 0x0B,
|
||||
0x0B, 0x0D, 0x09, 0x0E};
|
||||
|
||||
local RCON = {
|
||||
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
|
||||
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
|
||||
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
|
||||
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
|
||||
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
|
||||
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
|
||||
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
|
||||
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
|
||||
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
|
||||
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
|
||||
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
|
||||
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
|
||||
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
|
||||
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
|
||||
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
|
||||
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
|
||||
|
||||
|
||||
local GMUL = function(A, B)
|
||||
if(A == 0x01) then return B; end
|
||||
if(B == 0x01) then return A; end
|
||||
if(A == 0x00) then return 0; end
|
||||
if(B == 0x00) then return 0; end
|
||||
|
||||
local LA = LTABLE[A];
|
||||
local LB = LTABLE[B];
|
||||
|
||||
local sum = LA + LB;
|
||||
if (sum > 0xFF) then sum = sum - 0xFF; end
|
||||
|
||||
return ETABLE[sum];
|
||||
end
|
||||
|
||||
local byteSub = Array.substitute;
|
||||
|
||||
local shiftRow = Array.permute;
|
||||
|
||||
local mixCol = function(i, mix)
|
||||
local out = {};
|
||||
|
||||
local a, b, c, d;
|
||||
|
||||
a = GMUL(i[ 1], mix[ 1]);
|
||||
b = GMUL(i[ 2], mix[ 2]);
|
||||
c = GMUL(i[ 3], mix[ 3]);
|
||||
d = GMUL(i[ 4], mix[ 4]);
|
||||
out[ 1] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 5]);
|
||||
b = GMUL(i[ 2], mix[ 6]);
|
||||
c = GMUL(i[ 3], mix[ 7]);
|
||||
d = GMUL(i[ 4], mix[ 8]);
|
||||
out[ 2] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 9]);
|
||||
b = GMUL(i[ 2], mix[10]);
|
||||
c = GMUL(i[ 3], mix[11]);
|
||||
d = GMUL(i[ 4], mix[12]);
|
||||
out[ 3] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[13]);
|
||||
b = GMUL(i[ 2], mix[14]);
|
||||
c = GMUL(i[ 3], mix[15]);
|
||||
d = GMUL(i[ 4], mix[16]);
|
||||
out[ 4] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 5], mix[ 1]);
|
||||
b = GMUL(i[ 6], mix[ 2]);
|
||||
c = GMUL(i[ 7], mix[ 3]);
|
||||
d = GMUL(i[ 8], mix[ 4]);
|
||||
out[ 5] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 5]);
|
||||
b = GMUL(i[ 6], mix[ 6]);
|
||||
c = GMUL(i[ 7], mix[ 7]);
|
||||
d = GMUL(i[ 8], mix[ 8]);
|
||||
out[ 6] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 9]);
|
||||
b = GMUL(i[ 6], mix[10]);
|
||||
c = GMUL(i[ 7], mix[11]);
|
||||
d = GMUL(i[ 8], mix[12]);
|
||||
out[ 7] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[13]);
|
||||
b = GMUL(i[ 6], mix[14]);
|
||||
c = GMUL(i[ 7], mix[15]);
|
||||
d = GMUL(i[ 8], mix[16]);
|
||||
out[ 8] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 9], mix[ 1]);
|
||||
b = GMUL(i[10], mix[ 2]);
|
||||
c = GMUL(i[11], mix[ 3]);
|
||||
d = GMUL(i[12], mix[ 4]);
|
||||
out[ 9] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 5]);
|
||||
b = GMUL(i[10], mix[ 6]);
|
||||
c = GMUL(i[11], mix[ 7]);
|
||||
d = GMUL(i[12], mix[ 8]);
|
||||
out[10] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 9]);
|
||||
b = GMUL(i[10], mix[10]);
|
||||
c = GMUL(i[11], mix[11]);
|
||||
d = GMUL(i[12], mix[12]);
|
||||
out[11] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[13]);
|
||||
b = GMUL(i[10], mix[14]);
|
||||
c = GMUL(i[11], mix[15]);
|
||||
d = GMUL(i[12], mix[16]);
|
||||
out[12] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[13], mix[ 1]);
|
||||
b = GMUL(i[14], mix[ 2]);
|
||||
c = GMUL(i[15], mix[ 3]);
|
||||
d = GMUL(i[16], mix[ 4]);
|
||||
out[13] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 5]);
|
||||
b = GMUL(i[14], mix[ 6]);
|
||||
c = GMUL(i[15], mix[ 7]);
|
||||
d = GMUL(i[16], mix[ 8]);
|
||||
out[14] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 9]);
|
||||
b = GMUL(i[14], mix[10]);
|
||||
c = GMUL(i[15], mix[11]);
|
||||
d = GMUL(i[16], mix[12]);
|
||||
out[15] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[13]);
|
||||
b = GMUL(i[14], mix[14]);
|
||||
c = GMUL(i[15], mix[15]);
|
||||
d = GMUL(i[16], mix[16]);
|
||||
out[16] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyRound = function(key, round)
|
||||
local i = (round - 1) * 24;
|
||||
local out = key;
|
||||
|
||||
out[25 + i] = XOR(key[ 1 + i], XOR(SBOX[key[22 + i]], RCON[round]));
|
||||
out[26 + i] = XOR(key[ 2 + i], SBOX[key[23 + i]]);
|
||||
out[27 + i] = XOR(key[ 3 + i], SBOX[key[24 + i]]);
|
||||
out[28 + i] = XOR(key[ 4 + i], SBOX[key[21 + i]]);
|
||||
|
||||
out[29 + i] = XOR(out[25 + i], key[ 5 + i]);
|
||||
out[30 + i] = XOR(out[26 + i], key[ 6 + i]);
|
||||
out[31 + i] = XOR(out[27 + i], key[ 7 + i]);
|
||||
out[32 + i] = XOR(out[28 + i], key[ 8 + i]);
|
||||
|
||||
out[33 + i] = XOR(out[29 + i], key[ 9 + i]);
|
||||
out[34 + i] = XOR(out[30 + i], key[10 + i]);
|
||||
out[35 + i] = XOR(out[31 + i], key[11 + i]);
|
||||
out[36 + i] = XOR(out[32 + i], key[12 + i]);
|
||||
|
||||
out[37 + i] = XOR(out[33 + i], key[13 + i]);
|
||||
out[38 + i] = XOR(out[34 + i], key[14 + i]);
|
||||
out[39 + i] = XOR(out[35 + i], key[15 + i]);
|
||||
out[40 + i] = XOR(out[36 + i], key[16 + i]);
|
||||
|
||||
out[41 + i] = XOR(out[37 + i], key[17 + i]);
|
||||
out[42 + i] = XOR(out[38 + i], key[18 + i]);
|
||||
out[43 + i] = XOR(out[39 + i], key[19 + i]);
|
||||
out[44 + i] = XOR(out[40 + i], key[20 + i]);
|
||||
|
||||
out[45 + i] = XOR(out[41 + i], key[21 + i]);
|
||||
out[46 + i] = XOR(out[42 + i], key[22 + i]);
|
||||
out[47 + i] = XOR(out[43 + i], key[23 + i]);
|
||||
out[48 + i] = XOR(out[44 + i], key[24 + i]);
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyExpand = function(key)
|
||||
local bytes = Array.copy(key);
|
||||
|
||||
for i = 1, 8 do
|
||||
keyRound(bytes, i);
|
||||
end
|
||||
|
||||
local keys = {};
|
||||
|
||||
keys[ 1] = Array.slice(bytes, 1, 16);
|
||||
keys[ 2] = Array.slice(bytes, 17, 32);
|
||||
keys[ 3] = Array.slice(bytes, 33, 48);
|
||||
keys[ 4] = Array.slice(bytes, 49, 64);
|
||||
keys[ 5] = Array.slice(bytes, 65, 80);
|
||||
keys[ 6] = Array.slice(bytes, 81, 96);
|
||||
keys[ 7] = Array.slice(bytes, 97, 112);
|
||||
keys[ 8] = Array.slice(bytes, 113, 128);
|
||||
keys[ 9] = Array.slice(bytes, 129, 144);
|
||||
keys[10] = Array.slice(bytes, 145, 160);
|
||||
keys[11] = Array.slice(bytes, 161, 176);
|
||||
keys[12] = Array.slice(bytes, 177, 192);
|
||||
keys[13] = Array.slice(bytes, 193, 208);
|
||||
|
||||
return keys;
|
||||
|
||||
end
|
||||
|
||||
local addKey = Array.XOR;
|
||||
|
||||
|
||||
|
||||
local AES = {};
|
||||
|
||||
AES.blockSize = 16;
|
||||
|
||||
AES.encrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
--round 1
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[2]);
|
||||
|
||||
--round 2
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[3]);
|
||||
|
||||
--round 3
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[4]);
|
||||
|
||||
--round 4
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[5]);
|
||||
|
||||
--round 5
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[6]);
|
||||
|
||||
--round 6
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[7]);
|
||||
|
||||
--round 7
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[8]);
|
||||
|
||||
--round 8
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[9]);
|
||||
|
||||
--round 9
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[10]);
|
||||
|
||||
--round 10
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[11]);
|
||||
|
||||
--round 11
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[12]);
|
||||
|
||||
--round 12
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = addKey(block, key[13]);
|
||||
|
||||
return block;
|
||||
|
||||
end
|
||||
|
||||
AES.decrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[13]);
|
||||
|
||||
--round 1
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[12]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 2
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[11]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 3
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[10]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 4
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[9]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 5
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[8]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 6
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[7]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 7
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[6]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 8
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[5]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 9
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[4]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 10
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[3]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 11
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[2]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 12
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
return block;
|
||||
end
|
||||
|
||||
return AES;
|
||||
@@ -1,498 +0,0 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Bit = require("lockbox.util.bit");
|
||||
|
||||
local XOR = Bit.bxor;
|
||||
|
||||
local SBOX = {
|
||||
[0] = 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
|
||||
0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
|
||||
0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
|
||||
0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
|
||||
0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
|
||||
0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
|
||||
0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
|
||||
0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
|
||||
0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
|
||||
0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
|
||||
0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
|
||||
0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
|
||||
0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
|
||||
0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
|
||||
0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
|
||||
0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16};
|
||||
|
||||
local ISBOX = {
|
||||
[0] = 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
|
||||
0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
|
||||
0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
|
||||
0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
|
||||
0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
|
||||
0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
|
||||
0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
|
||||
0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
|
||||
0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
|
||||
0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
|
||||
0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
|
||||
0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
|
||||
0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
|
||||
0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
|
||||
0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
|
||||
0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D};
|
||||
|
||||
local ROW_SHIFT = { 1, 6, 11, 16, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, };
|
||||
local IROW_SHIFT = { 1, 14, 11, 8, 5, 2, 15, 12, 9, 6, 3, 16, 13, 10, 7, 4, };
|
||||
|
||||
local ETABLE = {
|
||||
[0] = 0x01, 0x03, 0x05, 0x0F, 0x11, 0x33, 0x55, 0xFF, 0x1A, 0x2E, 0x72, 0x96, 0xA1, 0xF8, 0x13, 0x35,
|
||||
0x5F, 0xE1, 0x38, 0x48, 0xD8, 0x73, 0x95, 0xA4, 0xF7, 0x02, 0x06, 0x0A, 0x1E, 0x22, 0x66, 0xAA,
|
||||
0xE5, 0x34, 0x5C, 0xE4, 0x37, 0x59, 0xEB, 0x26, 0x6A, 0xBE, 0xD9, 0x70, 0x90, 0xAB, 0xE6, 0x31,
|
||||
0x53, 0xF5, 0x04, 0x0C, 0x14, 0x3C, 0x44, 0xCC, 0x4F, 0xD1, 0x68, 0xB8, 0xD3, 0x6E, 0xB2, 0xCD,
|
||||
0x4C, 0xD4, 0x67, 0xA9, 0xE0, 0x3B, 0x4D, 0xD7, 0x62, 0xA6, 0xF1, 0x08, 0x18, 0x28, 0x78, 0x88,
|
||||
0x83, 0x9E, 0xB9, 0xD0, 0x6B, 0xBD, 0xDC, 0x7F, 0x81, 0x98, 0xB3, 0xCE, 0x49, 0xDB, 0x76, 0x9A,
|
||||
0xB5, 0xC4, 0x57, 0xF9, 0x10, 0x30, 0x50, 0xF0, 0x0B, 0x1D, 0x27, 0x69, 0xBB, 0xD6, 0x61, 0xA3,
|
||||
0xFE, 0x19, 0x2B, 0x7D, 0x87, 0x92, 0xAD, 0xEC, 0x2F, 0x71, 0x93, 0xAE, 0xE9, 0x20, 0x60, 0xA0,
|
||||
0xFB, 0x16, 0x3A, 0x4E, 0xD2, 0x6D, 0xB7, 0xC2, 0x5D, 0xE7, 0x32, 0x56, 0xFA, 0x15, 0x3F, 0x41,
|
||||
0xC3, 0x5E, 0xE2, 0x3D, 0x47, 0xC9, 0x40, 0xC0, 0x5B, 0xED, 0x2C, 0x74, 0x9C, 0xBF, 0xDA, 0x75,
|
||||
0x9F, 0xBA, 0xD5, 0x64, 0xAC, 0xEF, 0x2A, 0x7E, 0x82, 0x9D, 0xBC, 0xDF, 0x7A, 0x8E, 0x89, 0x80,
|
||||
0x9B, 0xB6, 0xC1, 0x58, 0xE8, 0x23, 0x65, 0xAF, 0xEA, 0x25, 0x6F, 0xB1, 0xC8, 0x43, 0xC5, 0x54,
|
||||
0xFC, 0x1F, 0x21, 0x63, 0xA5, 0xF4, 0x07, 0x09, 0x1B, 0x2D, 0x77, 0x99, 0xB0, 0xCB, 0x46, 0xCA,
|
||||
0x45, 0xCF, 0x4A, 0xDE, 0x79, 0x8B, 0x86, 0x91, 0xA8, 0xE3, 0x3E, 0x42, 0xC6, 0x51, 0xF3, 0x0E,
|
||||
0x12, 0x36, 0x5A, 0xEE, 0x29, 0x7B, 0x8D, 0x8C, 0x8F, 0x8A, 0x85, 0x94, 0xA7, 0xF2, 0x0D, 0x17,
|
||||
0x39, 0x4B, 0xDD, 0x7C, 0x84, 0x97, 0xA2, 0xFD, 0x1C, 0x24, 0x6C, 0xB4, 0xC7, 0x52, 0xF6, 0x01};
|
||||
|
||||
local LTABLE = {
|
||||
[0] = 0x00, 0x00, 0x19, 0x01, 0x32, 0x02, 0x1A, 0xC6, 0x4B, 0xC7, 0x1B, 0x68, 0x33, 0xEE, 0xDF, 0x03,
|
||||
0x64, 0x04, 0xE0, 0x0E, 0x34, 0x8D, 0x81, 0xEF, 0x4C, 0x71, 0x08, 0xC8, 0xF8, 0x69, 0x1C, 0xC1,
|
||||
0x7D, 0xC2, 0x1D, 0xB5, 0xF9, 0xB9, 0x27, 0x6A, 0x4D, 0xE4, 0xA6, 0x72, 0x9A, 0xC9, 0x09, 0x78,
|
||||
0x65, 0x2F, 0x8A, 0x05, 0x21, 0x0F, 0xE1, 0x24, 0x12, 0xF0, 0x82, 0x45, 0x35, 0x93, 0xDA, 0x8E,
|
||||
0x96, 0x8F, 0xDB, 0xBD, 0x36, 0xD0, 0xCE, 0x94, 0x13, 0x5C, 0xD2, 0xF1, 0x40, 0x46, 0x83, 0x38,
|
||||
0x66, 0xDD, 0xFD, 0x30, 0xBF, 0x06, 0x8B, 0x62, 0xB3, 0x25, 0xE2, 0x98, 0x22, 0x88, 0x91, 0x10,
|
||||
0x7E, 0x6E, 0x48, 0xC3, 0xA3, 0xB6, 0x1E, 0x42, 0x3A, 0x6B, 0x28, 0x54, 0xFA, 0x85, 0x3D, 0xBA,
|
||||
0x2B, 0x79, 0x0A, 0x15, 0x9B, 0x9F, 0x5E, 0xCA, 0x4E, 0xD4, 0xAC, 0xE5, 0xF3, 0x73, 0xA7, 0x57,
|
||||
0xAF, 0x58, 0xA8, 0x50, 0xF4, 0xEA, 0xD6, 0x74, 0x4F, 0xAE, 0xE9, 0xD5, 0xE7, 0xE6, 0xAD, 0xE8,
|
||||
0x2C, 0xD7, 0x75, 0x7A, 0xEB, 0x16, 0x0B, 0xF5, 0x59, 0xCB, 0x5F, 0xB0, 0x9C, 0xA9, 0x51, 0xA0,
|
||||
0x7F, 0x0C, 0xF6, 0x6F, 0x17, 0xC4, 0x49, 0xEC, 0xD8, 0x43, 0x1F, 0x2D, 0xA4, 0x76, 0x7B, 0xB7,
|
||||
0xCC, 0xBB, 0x3E, 0x5A, 0xFB, 0x60, 0xB1, 0x86, 0x3B, 0x52, 0xA1, 0x6C, 0xAA, 0x55, 0x29, 0x9D,
|
||||
0x97, 0xB2, 0x87, 0x90, 0x61, 0xBE, 0xDC, 0xFC, 0xBC, 0x95, 0xCF, 0xCD, 0x37, 0x3F, 0x5B, 0xD1,
|
||||
0x53, 0x39, 0x84, 0x3C, 0x41, 0xA2, 0x6D, 0x47, 0x14, 0x2A, 0x9E, 0x5D, 0x56, 0xF2, 0xD3, 0xAB,
|
||||
0x44, 0x11, 0x92, 0xD9, 0x23, 0x20, 0x2E, 0x89, 0xB4, 0x7C, 0xB8, 0x26, 0x77, 0x99, 0xE3, 0xA5,
|
||||
0x67, 0x4A, 0xED, 0xDE, 0xC5, 0x31, 0xFE, 0x18, 0x0D, 0x63, 0x8C, 0x80, 0xC0, 0xF7, 0x70, 0x07};
|
||||
|
||||
local MIXTABLE = {
|
||||
0x02, 0x03, 0x01, 0x01,
|
||||
0x01, 0x02, 0x03, 0x01,
|
||||
0x01, 0x01, 0x02, 0x03,
|
||||
0x03, 0x01, 0x01, 0x02};
|
||||
|
||||
local IMIXTABLE = {
|
||||
0x0E, 0x0B, 0x0D, 0x09,
|
||||
0x09, 0x0E, 0x0B, 0x0D,
|
||||
0x0D, 0x09, 0x0E, 0x0B,
|
||||
0x0B, 0x0D, 0x09, 0x0E};
|
||||
|
||||
local RCON = {
|
||||
[0] = 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
|
||||
0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
|
||||
0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
|
||||
0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
|
||||
0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
|
||||
0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
|
||||
0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
|
||||
0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
|
||||
0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
|
||||
0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
|
||||
0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
|
||||
0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
|
||||
0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
|
||||
0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
|
||||
0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
|
||||
0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d};
|
||||
|
||||
|
||||
local GMUL = function(A, B)
|
||||
if(A == 0x01) then return B; end
|
||||
if(B == 0x01) then return A; end
|
||||
if(A == 0x00) then return 0; end
|
||||
if(B == 0x00) then return 0; end
|
||||
|
||||
local LA = LTABLE[A];
|
||||
local LB = LTABLE[B];
|
||||
|
||||
local sum = LA + LB;
|
||||
if (sum > 0xFF) then sum = sum - 0xFF; end
|
||||
|
||||
return ETABLE[sum];
|
||||
end
|
||||
|
||||
local byteSub = Array.substitute;
|
||||
|
||||
local shiftRow = Array.permute;
|
||||
|
||||
local mixCol = function(i, mix)
|
||||
local out = {};
|
||||
|
||||
local a, b, c, d;
|
||||
|
||||
a = GMUL(i[ 1], mix[ 1]);
|
||||
b = GMUL(i[ 2], mix[ 2]);
|
||||
c = GMUL(i[ 3], mix[ 3]);
|
||||
d = GMUL(i[ 4], mix[ 4]);
|
||||
out[ 1] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 5]);
|
||||
b = GMUL(i[ 2], mix[ 6]);
|
||||
c = GMUL(i[ 3], mix[ 7]);
|
||||
d = GMUL(i[ 4], mix[ 8]);
|
||||
out[ 2] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[ 9]);
|
||||
b = GMUL(i[ 2], mix[10]);
|
||||
c = GMUL(i[ 3], mix[11]);
|
||||
d = GMUL(i[ 4], mix[12]);
|
||||
out[ 3] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 1], mix[13]);
|
||||
b = GMUL(i[ 2], mix[14]);
|
||||
c = GMUL(i[ 3], mix[15]);
|
||||
d = GMUL(i[ 4], mix[16]);
|
||||
out[ 4] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 5], mix[ 1]);
|
||||
b = GMUL(i[ 6], mix[ 2]);
|
||||
c = GMUL(i[ 7], mix[ 3]);
|
||||
d = GMUL(i[ 8], mix[ 4]);
|
||||
out[ 5] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 5]);
|
||||
b = GMUL(i[ 6], mix[ 6]);
|
||||
c = GMUL(i[ 7], mix[ 7]);
|
||||
d = GMUL(i[ 8], mix[ 8]);
|
||||
out[ 6] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[ 9]);
|
||||
b = GMUL(i[ 6], mix[10]);
|
||||
c = GMUL(i[ 7], mix[11]);
|
||||
d = GMUL(i[ 8], mix[12]);
|
||||
out[ 7] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 5], mix[13]);
|
||||
b = GMUL(i[ 6], mix[14]);
|
||||
c = GMUL(i[ 7], mix[15]);
|
||||
d = GMUL(i[ 8], mix[16]);
|
||||
out[ 8] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[ 9], mix[ 1]);
|
||||
b = GMUL(i[10], mix[ 2]);
|
||||
c = GMUL(i[11], mix[ 3]);
|
||||
d = GMUL(i[12], mix[ 4]);
|
||||
out[ 9] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 5]);
|
||||
b = GMUL(i[10], mix[ 6]);
|
||||
c = GMUL(i[11], mix[ 7]);
|
||||
d = GMUL(i[12], mix[ 8]);
|
||||
out[10] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[ 9]);
|
||||
b = GMUL(i[10], mix[10]);
|
||||
c = GMUL(i[11], mix[11]);
|
||||
d = GMUL(i[12], mix[12]);
|
||||
out[11] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[ 9], mix[13]);
|
||||
b = GMUL(i[10], mix[14]);
|
||||
c = GMUL(i[11], mix[15]);
|
||||
d = GMUL(i[12], mix[16]);
|
||||
out[12] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
|
||||
a = GMUL(i[13], mix[ 1]);
|
||||
b = GMUL(i[14], mix[ 2]);
|
||||
c = GMUL(i[15], mix[ 3]);
|
||||
d = GMUL(i[16], mix[ 4]);
|
||||
out[13] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 5]);
|
||||
b = GMUL(i[14], mix[ 6]);
|
||||
c = GMUL(i[15], mix[ 7]);
|
||||
d = GMUL(i[16], mix[ 8]);
|
||||
out[14] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[ 9]);
|
||||
b = GMUL(i[14], mix[10]);
|
||||
c = GMUL(i[15], mix[11]);
|
||||
d = GMUL(i[16], mix[12]);
|
||||
out[15] = XOR(XOR(a, b), XOR(c, d));
|
||||
a = GMUL(i[13], mix[13]);
|
||||
b = GMUL(i[14], mix[14]);
|
||||
c = GMUL(i[15], mix[15]);
|
||||
d = GMUL(i[16], mix[16]);
|
||||
out[16] = XOR(XOR(a, b), XOR(c, d));
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyRound = function(key, round)
|
||||
local i = (round - 1) * 32;
|
||||
local out = key;
|
||||
|
||||
out[33 + i] = XOR(key[ 1 + i], XOR(SBOX[key[30 + i]], RCON[round]));
|
||||
out[34 + i] = XOR(key[ 2 + i], SBOX[key[31 + i]]);
|
||||
out[35 + i] = XOR(key[ 3 + i], SBOX[key[32 + i]]);
|
||||
out[36 + i] = XOR(key[ 4 + i], SBOX[key[29 + i]]);
|
||||
|
||||
out[37 + i] = XOR(out[33 + i], key[ 5 + i]);
|
||||
out[38 + i] = XOR(out[34 + i], key[ 6 + i]);
|
||||
out[39 + i] = XOR(out[35 + i], key[ 7 + i]);
|
||||
out[40 + i] = XOR(out[36 + i], key[ 8 + i]);
|
||||
|
||||
out[41 + i] = XOR(out[37 + i], key[ 9 + i]);
|
||||
out[42 + i] = XOR(out[38 + i], key[10 + i]);
|
||||
out[43 + i] = XOR(out[39 + i], key[11 + i]);
|
||||
out[44 + i] = XOR(out[40 + i], key[12 + i]);
|
||||
|
||||
out[45 + i] = XOR(out[41 + i], key[13 + i]);
|
||||
out[46 + i] = XOR(out[42 + i], key[14 + i]);
|
||||
out[47 + i] = XOR(out[43 + i], key[15 + i]);
|
||||
out[48 + i] = XOR(out[44 + i], key[16 + i]);
|
||||
|
||||
|
||||
out[49 + i] = XOR(SBOX[out[45 + i]], key[17 + i]);
|
||||
out[50 + i] = XOR(SBOX[out[46 + i]], key[18 + i]);
|
||||
out[51 + i] = XOR(SBOX[out[47 + i]], key[19 + i]);
|
||||
out[52 + i] = XOR(SBOX[out[48 + i]], key[20 + i]);
|
||||
|
||||
out[53 + i] = XOR(out[49 + i], key[21 + i]);
|
||||
out[54 + i] = XOR(out[50 + i], key[22 + i]);
|
||||
out[55 + i] = XOR(out[51 + i], key[23 + i]);
|
||||
out[56 + i] = XOR(out[52 + i], key[24 + i]);
|
||||
|
||||
out[57 + i] = XOR(out[53 + i], key[25 + i]);
|
||||
out[58 + i] = XOR(out[54 + i], key[26 + i]);
|
||||
out[59 + i] = XOR(out[55 + i], key[27 + i]);
|
||||
out[60 + i] = XOR(out[56 + i], key[28 + i]);
|
||||
|
||||
out[61 + i] = XOR(out[57 + i], key[29 + i]);
|
||||
out[62 + i] = XOR(out[58 + i], key[30 + i]);
|
||||
out[63 + i] = XOR(out[59 + i], key[31 + i]);
|
||||
out[64 + i] = XOR(out[60 + i], key[32 + i]);
|
||||
|
||||
return out;
|
||||
end
|
||||
|
||||
local keyExpand = function(key)
|
||||
local bytes = Array.copy(key);
|
||||
|
||||
for i = 1, 7 do
|
||||
keyRound(bytes, i);
|
||||
end
|
||||
|
||||
local keys = {};
|
||||
|
||||
keys[ 1] = Array.slice(bytes, 1, 16);
|
||||
keys[ 2] = Array.slice(bytes, 17, 32);
|
||||
keys[ 3] = Array.slice(bytes, 33, 48);
|
||||
keys[ 4] = Array.slice(bytes, 49, 64);
|
||||
keys[ 5] = Array.slice(bytes, 65, 80);
|
||||
keys[ 6] = Array.slice(bytes, 81, 96);
|
||||
keys[ 7] = Array.slice(bytes, 97, 112);
|
||||
keys[ 8] = Array.slice(bytes, 113, 128);
|
||||
keys[ 9] = Array.slice(bytes, 129, 144);
|
||||
keys[10] = Array.slice(bytes, 145, 160);
|
||||
keys[11] = Array.slice(bytes, 161, 176);
|
||||
keys[12] = Array.slice(bytes, 177, 192);
|
||||
keys[13] = Array.slice(bytes, 193, 208);
|
||||
keys[14] = Array.slice(bytes, 209, 224);
|
||||
keys[15] = Array.slice(bytes, 225, 240);
|
||||
|
||||
return keys;
|
||||
|
||||
end
|
||||
|
||||
local addKey = Array.XOR;
|
||||
|
||||
|
||||
|
||||
local AES = {};
|
||||
|
||||
AES.blockSize = 16;
|
||||
|
||||
AES.encrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
--round 1
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[2]);
|
||||
|
||||
--round 2
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[3]);
|
||||
|
||||
--round 3
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[4]);
|
||||
|
||||
--round 4
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[5]);
|
||||
|
||||
--round 5
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[6]);
|
||||
|
||||
--round 6
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[7]);
|
||||
|
||||
--round 7
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[8]);
|
||||
|
||||
--round 8
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[9]);
|
||||
|
||||
--round 9
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[10]);
|
||||
|
||||
--round 10
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[11]);
|
||||
|
||||
--round 11
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[12]);
|
||||
|
||||
--round 12
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[13]);
|
||||
|
||||
--round 13
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = mixCol(block, MIXTABLE);
|
||||
block = addKey(block, key[14]);
|
||||
|
||||
--round 14
|
||||
block = byteSub(block, SBOX);
|
||||
block = shiftRow(block, ROW_SHIFT);
|
||||
block = addKey(block, key[15]);
|
||||
|
||||
return block;
|
||||
|
||||
end
|
||||
|
||||
AES.decrypt = function(_key, block)
|
||||
|
||||
local key = keyExpand(_key);
|
||||
|
||||
--round 0
|
||||
block = addKey(block, key[15]);
|
||||
|
||||
--round 1
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[14]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 2
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[13]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 3
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[12]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 4
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[11]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 5
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[10]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 6
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[9]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 7
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[8]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 8
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[7]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 9
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[6]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 10
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[5]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 11
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[4]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 12
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[3]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 13
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[2]);
|
||||
block = mixCol(block, IMIXTABLE);
|
||||
|
||||
--round 14
|
||||
block = shiftRow(block, IROW_SHIFT);
|
||||
block = byteSub(block, ISBOX);
|
||||
block = addKey(block, key[1]);
|
||||
|
||||
return block;
|
||||
end
|
||||
|
||||
return AES;
|
||||
@@ -1,164 +0,0 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local CBC = {};
|
||||
|
||||
CBC.Cipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = Array.XOR(iv, block);
|
||||
out = blockCipher.encrypt(key, out);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
iv = out;
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
|
||||
CBC.Decipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = block;
|
||||
out = blockCipher.decrypt(key, out);
|
||||
out = Array.XOR(iv, out);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
iv = block;
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return CBC;
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local CFB = {};
|
||||
|
||||
CFB.Cipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
iv = out;
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
CFB.Decipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
iv = block;
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return CFB;
|
||||
@@ -1,248 +0,0 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local Bit = require("lockbox.util.bit");
|
||||
|
||||
local AND = Bit.band;
|
||||
|
||||
local CTR = {};
|
||||
|
||||
CTR.Cipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
local updateIV = function()
|
||||
iv[16] = iv[16] + 1;
|
||||
if iv[16] <= 0xFF then return; end
|
||||
iv[16] = AND(iv[16], 0xFF);
|
||||
|
||||
iv[15] = iv[15] + 1;
|
||||
if iv[15] <= 0xFF then return; end
|
||||
iv[15] = AND(iv[15], 0xFF);
|
||||
|
||||
iv[14] = iv[14] + 1;
|
||||
if iv[14] <= 0xFF then return; end
|
||||
iv[14] = AND(iv[14], 0xFF);
|
||||
|
||||
iv[13] = iv[13] + 1;
|
||||
if iv[13] <= 0xFF then return; end
|
||||
iv[13] = AND(iv[13], 0xFF);
|
||||
|
||||
iv[12] = iv[12] + 1;
|
||||
if iv[12] <= 0xFF then return; end
|
||||
iv[12] = AND(iv[12], 0xFF);
|
||||
|
||||
iv[11] = iv[11] + 1;
|
||||
if iv[11] <= 0xFF then return; end
|
||||
iv[11] = AND(iv[11], 0xFF);
|
||||
|
||||
iv[10] = iv[10] + 1;
|
||||
if iv[10] <= 0xFF then return; end
|
||||
iv[10] = AND(iv[10], 0xFF);
|
||||
|
||||
iv[9] = iv[9] + 1;
|
||||
if iv[9] <= 0xFF then return; end
|
||||
iv[9] = AND(iv[9], 0xFF);
|
||||
|
||||
return;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
updateIV();
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
|
||||
CTR.Decipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
local updateIV = function()
|
||||
iv[16] = iv[16] + 1;
|
||||
if iv[16] <= 0xFF then return; end
|
||||
iv[16] = AND(iv[16], 0xFF);
|
||||
|
||||
iv[15] = iv[15] + 1;
|
||||
if iv[15] <= 0xFF then return; end
|
||||
iv[15] = AND(iv[15], 0xFF);
|
||||
|
||||
iv[14] = iv[14] + 1;
|
||||
if iv[14] <= 0xFF then return; end
|
||||
iv[14] = AND(iv[14], 0xFF);
|
||||
|
||||
iv[13] = iv[13] + 1;
|
||||
if iv[13] <= 0xFF then return; end
|
||||
iv[13] = AND(iv[13], 0xFF);
|
||||
|
||||
iv[12] = iv[12] + 1;
|
||||
if iv[12] <= 0xFF then return; end
|
||||
iv[12] = AND(iv[12], 0xFF);
|
||||
|
||||
iv[11] = iv[11] + 1;
|
||||
if iv[11] <= 0xFF then return; end
|
||||
iv[11] = AND(iv[11], 0xFF);
|
||||
|
||||
iv[10] = iv[10] + 1;
|
||||
if iv[10] <= 0xFF then return; end
|
||||
iv[10] = AND(iv[10], 0xFF);
|
||||
|
||||
iv[9] = iv[9] + 1;
|
||||
if iv[9] <= 0xFF then return; end
|
||||
iv[9] = AND(iv[9], 0xFF);
|
||||
|
||||
return;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
updateIV();
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
|
||||
return CTR;
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
local Array = require("lockbox.util.array");
|
||||
local Stream = require("lockbox.util.stream");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local OFB = {};
|
||||
|
||||
OFB.Cipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
iv = out;
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
OFB.Decipher = function()
|
||||
|
||||
local public = {};
|
||||
|
||||
local key;
|
||||
local blockCipher;
|
||||
local padding;
|
||||
local inputQueue;
|
||||
local outputQueue;
|
||||
local iv;
|
||||
|
||||
public.setKey = function(keyBytes)
|
||||
key = keyBytes;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setBlockCipher = function(cipher)
|
||||
blockCipher = cipher;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.setPadding = function(paddingMode)
|
||||
padding = paddingMode;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
inputQueue = Queue();
|
||||
outputQueue = Queue();
|
||||
iv = nil;
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(messageStream)
|
||||
local byte = messageStream();
|
||||
while (byte ~= nil) do
|
||||
inputQueue.push(byte);
|
||||
if(inputQueue.size() >= blockCipher.blockSize) then
|
||||
local block = Array.readFromQueue(inputQueue, blockCipher.blockSize);
|
||||
|
||||
if(iv == nil) then
|
||||
iv = block;
|
||||
else
|
||||
local out = iv;
|
||||
out = blockCipher.encrypt(key, out);
|
||||
iv = out;
|
||||
out = Array.XOR(out, block);
|
||||
Array.writeToQueue(outputQueue, out);
|
||||
end
|
||||
end
|
||||
byte = messageStream();
|
||||
end
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local paddingStream = padding(blockCipher.blockSize, inputQueue.getHead());
|
||||
public.update(paddingStream);
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.getOutputQueue = function()
|
||||
return outputQueue;
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
return Stream.toHex(outputQueue.pop);
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
return Stream.toArray(outputQueue.pop);
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
|
||||
return OFB;
|
||||
199
lockbox/digest/md5.lua
Normal file
199
lockbox/digest/md5.lua
Normal file
@@ -0,0 +1,199 @@
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
local SHIFT = {
|
||||
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
|
||||
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
|
||||
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
|
||||
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21};
|
||||
|
||||
local CONSTANTS = {
|
||||
0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
|
||||
0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
|
||||
0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
|
||||
0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
|
||||
0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
|
||||
0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
|
||||
0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
|
||||
0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
|
||||
0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
|
||||
0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
|
||||
0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
|
||||
0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
|
||||
0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
|
||||
0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
|
||||
0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
|
||||
0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391};
|
||||
|
||||
local AND = Bit.band;
|
||||
local OR = Bit.bor;
|
||||
local NOT = Bit.bnot;
|
||||
local XOR = Bit.bxor;
|
||||
local LROT = Bit.lrotate;
|
||||
local LSHIFT = Bit.lshift;
|
||||
local RSHIFT = Bit.rshift;
|
||||
|
||||
--MD5 is little-endian
|
||||
local bytes2word = function(b0, b1, b2, b3)
|
||||
local i = b3; i = LSHIFT(i, 8);
|
||||
i = OR(i, b2); i = LSHIFT(i, 8);
|
||||
i = OR(i, b1); i = LSHIFT(i, 8);
|
||||
i = OR(i, b0);
|
||||
return i;
|
||||
end
|
||||
|
||||
local word2bytes = function(word)
|
||||
local b0, b1, b2, b3;
|
||||
b0 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b1 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b2 = AND(word, 0xFF); word = RSHIFT(word, 8);
|
||||
b3 = AND(word, 0xFF);
|
||||
return b0, b1, b2, b3;
|
||||
end
|
||||
|
||||
local dword2bytes = function(i)
|
||||
local b4, b5, b6, b7 = word2bytes(Math.floor(i / 0x100000000));
|
||||
local b0, b1, b2, b3 = word2bytes(i);
|
||||
return b0, b1, b2, b3, b4, b5, b6, b7;
|
||||
end
|
||||
|
||||
local F = function(x, y, z) return OR(AND(x, y), AND(NOT(x), z)); end
|
||||
local G = function(x, y, z) return OR(AND(x, z), AND(y, NOT(z))); end
|
||||
local H = function(x, y, z) return XOR(x, XOR(y, z)); end
|
||||
local I = function(x, y, z) return XOR(y, OR(x, NOT(z))); end
|
||||
|
||||
local MD5 = function()
|
||||
|
||||
local queue = Queue();
|
||||
|
||||
local A = 0x67452301;
|
||||
local B = 0xefcdab89;
|
||||
local C = 0x98badcfe;
|
||||
local D = 0x10325476;
|
||||
local public = {};
|
||||
|
||||
local processBlock = function()
|
||||
local a = A;
|
||||
local b = B;
|
||||
local c = C;
|
||||
local d = D;
|
||||
|
||||
local X = {};
|
||||
|
||||
for i = 1, 16 do
|
||||
X[i] = bytes2word(queue.pop(), queue.pop(), queue.pop(), queue.pop());
|
||||
end
|
||||
|
||||
for i = 0, 63 do
|
||||
local f, g, temp;
|
||||
|
||||
if (0 <= i) and (i <= 15) then
|
||||
f = F(b, c, d);
|
||||
g = i;
|
||||
elseif (16 <= i) and (i <= 31) then
|
||||
f = G(b, c, d);
|
||||
g = (5 * i + 1) % 16;
|
||||
elseif (32 <= i) and (i <= 47) then
|
||||
f = H(b, c, d);
|
||||
g = (3 * i + 5) % 16;
|
||||
elseif (48 <= i) and (i <= 63) then
|
||||
f = I(b, c, d);
|
||||
g = (7 * i) % 16;
|
||||
end
|
||||
temp = d;
|
||||
d = c;
|
||||
c = b;
|
||||
b = b + LROT((a + f + CONSTANTS[i + 1] + X[g + 1]), SHIFT[i + 1]);
|
||||
a = temp;
|
||||
end
|
||||
|
||||
A = AND(A + a, 0xFFFFFFFF);
|
||||
B = AND(B + b, 0xFFFFFFFF);
|
||||
C = AND(C + c, 0xFFFFFFFF);
|
||||
D = AND(D + d, 0xFFFFFFFF);
|
||||
end
|
||||
|
||||
public.init = function()
|
||||
queue.reset();
|
||||
|
||||
A = 0x67452301;
|
||||
B = 0xefcdab89;
|
||||
C = 0x98badcfe;
|
||||
D = 0x10325476;
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.update = function(bytes)
|
||||
for b in bytes do
|
||||
queue.push(b);
|
||||
if(queue.size() >= 64) then processBlock(); end
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.finish = function()
|
||||
local bits = queue.getHead() * 8;
|
||||
|
||||
queue.push(0x80);
|
||||
while ((queue.size() + 7) % 64) < 63 do
|
||||
queue.push(0x00);
|
||||
end
|
||||
|
||||
local b0, b1, b2, b3, b4, b5, b6, b7 = dword2bytes(bits);
|
||||
|
||||
queue.push(b0);
|
||||
queue.push(b1);
|
||||
queue.push(b2);
|
||||
queue.push(b3);
|
||||
queue.push(b4);
|
||||
queue.push(b5);
|
||||
queue.push(b6);
|
||||
queue.push(b7);
|
||||
|
||||
while queue.size() > 0 do
|
||||
processBlock();
|
||||
end
|
||||
|
||||
return public;
|
||||
end
|
||||
|
||||
public.asBytes = function()
|
||||
local b0, b1, b2, b3 = word2bytes(A);
|
||||
local b4, b5, b6, b7 = word2bytes(B);
|
||||
local b8, b9, b10, b11 = word2bytes(C);
|
||||
local b12, b13, b14, b15 = word2bytes(D);
|
||||
|
||||
return {b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15};
|
||||
end
|
||||
|
||||
public.asHex = function()
|
||||
local b0, b1, b2, b3 = word2bytes(A);
|
||||
local b4, b5, b6, b7 = word2bytes(B);
|
||||
local b8, b9, b10, b11 = word2bytes(C);
|
||||
local b12, b13, b14, b15 = word2bytes(D);
|
||||
|
||||
return String.format("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15);
|
||||
end
|
||||
|
||||
public.asString = function()
|
||||
local b0, b1, b2, b3 = word2bytes(A);
|
||||
local b4, b5, b6, b7 = word2bytes(B);
|
||||
local b8, b9, b10, b11 = word2bytes(C);
|
||||
local b12, b13, b14, b15 = word2bytes(D);
|
||||
|
||||
return string.pack(string.rep('B', 16),
|
||||
b0, b1, b2, b3, b4, b5, b6, b7, b8,
|
||||
b9, b10, b11, b12, b13, b14, b15
|
||||
)
|
||||
end
|
||||
|
||||
return public;
|
||||
|
||||
end
|
||||
|
||||
return MD5;
|
||||
@@ -1,5 +1,3 @@
|
||||
require("lockbox").insecure();
|
||||
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local String = require("string");
|
||||
local Math = require("math");
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
local Lockbox = {};
|
||||
local Lockbox = {}
|
||||
|
||||
--[[
|
||||
package.path = "./?.lua;"
|
||||
.. "./cipher/?.lua;"
|
||||
.. "./digest/?.lua;"
|
||||
.. "./kdf/?.lua;"
|
||||
.. "./mac/?.lua;"
|
||||
.. "./padding/?.lua;"
|
||||
.. "./test/?.lua;"
|
||||
.. "./util/?.lua;"
|
||||
.. package.path;
|
||||
--]]
|
||||
Lockbox.ALLOW_INSECURE = true;
|
||||
-- cc-mek-scada lockbox version
|
||||
Lockbox.version = "1.1"
|
||||
|
||||
Lockbox.insecure = function()
|
||||
assert(Lockbox.ALLOW_INSECURE,
|
||||
"This module is insecure! It should not be used in production." ..
|
||||
"If you really want to use it, set Lockbox.ALLOW_INSECURE to true before importing it");
|
||||
end
|
||||
|
||||
return Lockbox;
|
||||
return Lockbox
|
||||
|
||||
@@ -8,7 +8,7 @@ local HMAC = function()
|
||||
|
||||
local public = {};
|
||||
local blockSize = 64;
|
||||
local Digest = nil;
|
||||
local Digest;
|
||||
local outerPadding = {};
|
||||
local innerPadding = {}
|
||||
local digest;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
local ANSIX923Padding = function(blockSize, byteCount)
|
||||
|
||||
local paddingCount = blockSize - (byteCount % blockSize);
|
||||
local bytesLeft = paddingCount;
|
||||
|
||||
local stream = function()
|
||||
if bytesLeft > 1 then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return 0x00;
|
||||
elseif bytesLeft > 0 then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return paddingCount;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
return stream;
|
||||
|
||||
end
|
||||
|
||||
return ANSIX923Padding;
|
||||
@@ -1,22 +0,0 @@
|
||||
local ISOIEC7816Padding = function(blockSize, byteCount)
|
||||
|
||||
local paddingCount = blockSize - (byteCount % blockSize);
|
||||
local bytesLeft = paddingCount;
|
||||
|
||||
local stream = function()
|
||||
if bytesLeft == paddingCount then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return 0x80;
|
||||
elseif bytesLeft > 0 then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return 0x00;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
return stream;
|
||||
|
||||
end
|
||||
|
||||
return ISOIEC7816Padding;
|
||||
@@ -1,18 +0,0 @@
|
||||
local PKCS7Padding = function(blockSize, byteCount)
|
||||
|
||||
local paddingCount = blockSize - ((byteCount -1) % blockSize) + 1;
|
||||
local bytesLeft = paddingCount;
|
||||
|
||||
local stream = function()
|
||||
if bytesLeft > 0 then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return paddingCount;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
return stream;
|
||||
end
|
||||
|
||||
return PKCS7Padding;
|
||||
@@ -1,19 +0,0 @@
|
||||
local ZeroPadding = function(blockSize, byteCount)
|
||||
|
||||
local paddingCount = blockSize - ((byteCount -1) % blockSize) + 1;
|
||||
local bytesLeft = paddingCount;
|
||||
|
||||
local stream = function()
|
||||
if bytesLeft > 0 then
|
||||
bytesLeft = bytesLeft - 1;
|
||||
return 0x00;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
return stream;
|
||||
|
||||
end
|
||||
|
||||
return ZeroPadding;
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
local String = require("string");
|
||||
local Bit = require("lockbox.util.bit");
|
||||
local Queue = require("lockbox.util.queue");
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
local ok, e
|
||||
ok = nil
|
||||
if not ok then
|
||||
ok, e = pcall(require, "bit") -- the LuaJIT one ?
|
||||
end
|
||||
-- modified (simplified) for ComputerCraft
|
||||
|
||||
local ok, e = nil, nil
|
||||
|
||||
if not ok then
|
||||
ok, e = pcall(require, "bit32") -- Lua 5.2
|
||||
end
|
||||
|
||||
if not ok then
|
||||
ok, e = pcall(require, "bit.numberlua") -- for Lua 5.1, https://github.com/tst2005/lua-bit-numberlua/
|
||||
ok, e = pcall(require, "bit")
|
||||
end
|
||||
|
||||
if not ok then
|
||||
error("no bitwise support found", 2)
|
||||
end
|
||||
|
||||
assert(type(e) == "table", "invalid bit module")
|
||||
|
||||
-- Workaround to support Lua 5.2 bit32 API with the LuaJIT bit one
|
||||
if e.rol and not e.lrotate then
|
||||
e.lrotate = e.rol
|
||||
end
|
||||
if e.ror and not e.rrotate then
|
||||
e.rrotate = e.ror
|
||||
end
|
||||
|
||||
return e
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
local config = {}
|
||||
|
||||
-- port of the SCADA supervisor
|
||||
config.SCADA_SV_PORT = 16100
|
||||
-- port for SCADA coordinator API access
|
||||
config.SCADA_API_PORT = 16200
|
||||
-- port to listen to incoming packets FROM servers
|
||||
config.LISTEN_PORT = 16201
|
||||
-- max trusted modem message distance (0 to disable check)
|
||||
config.TRUSTED_RANGE = 0
|
||||
-- time in seconds (>= 2) before assuming a remote device is no longer active
|
||||
config.COMMS_TIMEOUT = 5
|
||||
|
||||
-- log path
|
||||
config.LOG_PATH = "/log.txt"
|
||||
-- log mode
|
||||
-- 0 = APPEND (adds to existing file on start)
|
||||
-- 1 = NEW (replaces existing file on start)
|
||||
config.LOG_MODE = 0
|
||||
-- true to log verbose debug messages
|
||||
config.LOG_DEBUG = false
|
||||
|
||||
return config
|
||||
578
pocket/configure.lua
Normal file
578
pocket/configure.lua
Normal file
@@ -0,0 +1,578 @@
|
||||
--
|
||||
-- Configuration GUI
|
||||
--
|
||||
|
||||
local log = require("scada-common.log")
|
||||
local tcd = require("scada-common.tcd")
|
||||
local util = require("scada-common.util")
|
||||
|
||||
local core = require("graphics.core")
|
||||
|
||||
local DisplayBox = require("graphics.elements.displaybox")
|
||||
local Div = require("graphics.elements.div")
|
||||
local ListBox = require("graphics.elements.listbox")
|
||||
local MultiPane = require("graphics.elements.multipane")
|
||||
local TextBox = require("graphics.elements.textbox")
|
||||
|
||||
local CheckBox = require("graphics.elements.controls.checkbox")
|
||||
local PushButton = require("graphics.elements.controls.push_button")
|
||||
local RadioButton = require("graphics.elements.controls.radio_button")
|
||||
|
||||
local NumberField = require("graphics.elements.form.number_field")
|
||||
local TextField = require("graphics.elements.form.text_field")
|
||||
|
||||
local println = util.println
|
||||
local tri = util.trinary
|
||||
|
||||
local cpair = core.cpair
|
||||
|
||||
local LEFT = core.ALIGN.LEFT
|
||||
local CENTER = core.ALIGN.CENTER
|
||||
local RIGHT = core.ALIGN.RIGHT
|
||||
|
||||
-- changes to the config data/format to let the user know
|
||||
local changes = {}
|
||||
|
||||
---@class pkt_configurator
|
||||
local configurator = {}
|
||||
|
||||
local style = {}
|
||||
|
||||
style.root = cpair(colors.black, colors.lightGray)
|
||||
style.header = cpair(colors.white, colors.gray)
|
||||
|
||||
style.colors = {
|
||||
{ c = colors.red, hex = 0xdf4949 },
|
||||
{ c = colors.orange, hex = 0xffb659 },
|
||||
{ c = colors.yellow, hex = 0xfffc79 },
|
||||
{ c = colors.lime, hex = 0x80ff80 },
|
||||
{ c = colors.green, hex = 0x4aee8a },
|
||||
{ c = colors.cyan, hex = 0x34bac8 },
|
||||
{ c = colors.lightBlue, hex = 0x6cc0f2 },
|
||||
{ c = colors.blue, hex = 0x0096ff },
|
||||
{ c = colors.purple, hex = 0xb156ee },
|
||||
{ c = colors.pink, hex = 0xf26ba2 },
|
||||
{ c = colors.magenta, hex = 0xf9488a },
|
||||
{ c = colors.lightGray, hex = 0xcacaca },
|
||||
{ c = colors.gray, hex = 0x575757 }
|
||||
}
|
||||
|
||||
local bw_fg_bg = cpair(colors.black, colors.white)
|
||||
local g_lg_fg_bg = cpair(colors.gray, colors.lightGray)
|
||||
local nav_fg_bg = bw_fg_bg
|
||||
local btn_act_fg_bg = cpair(colors.white, colors.gray)
|
||||
local dis_fg_bg = cpair(colors.lightGray,colors.white)
|
||||
|
||||
local tool_ctl = {
|
||||
ask_config = false,
|
||||
has_config = false,
|
||||
viewing_config = false,
|
||||
importing_legacy = false,
|
||||
|
||||
view_cfg = nil, ---@type graphics_element
|
||||
settings_apply = nil, ---@type graphics_element
|
||||
|
||||
set_networked = nil, ---@type function
|
||||
bundled_emcool = nil, ---@type function
|
||||
gen_summary = nil, ---@type function
|
||||
show_current_cfg = nil, ---@type function
|
||||
load_legacy = nil, ---@type function
|
||||
|
||||
show_auth_key = nil, ---@type function
|
||||
show_key_btn = nil, ---@type graphics_element
|
||||
auth_key_textbox = nil, ---@type graphics_element
|
||||
auth_key_value = ""
|
||||
}
|
||||
|
||||
---@class pkt_config
|
||||
local tmp_cfg = {
|
||||
SVR_Channel = nil, ---@type integer
|
||||
CRD_Channel = nil, ---@type integer
|
||||
PKT_Channel = nil, ---@type integer
|
||||
ConnTimeout = nil, ---@type number
|
||||
TrustedRange = nil, ---@type number
|
||||
AuthKey = nil, ---@type string|nil
|
||||
LogMode = 0,
|
||||
LogPath = "",
|
||||
LogDebug = false,
|
||||
}
|
||||
|
||||
---@class pkt_config
|
||||
local ini_cfg = {}
|
||||
---@class pkt_config
|
||||
local settings_cfg = {}
|
||||
|
||||
-- all settings fields, their nice names, and their default values
|
||||
local fields = {
|
||||
{ "SVR_Channel", "SVR Channel", 16240 },
|
||||
{ "CRD_Channel", "CRD Channel", 16243 },
|
||||
{ "PKT_Channel", "PKT Channel", 16244 },
|
||||
{ "ConnTimeout", "Connection Timeout", 5 },
|
||||
{ "TrustedRange", "Trusted Range", 0 },
|
||||
{ "AuthKey", "Facility Auth Key" , ""},
|
||||
{ "LogMode", "Log Mode", log.MODE.APPEND },
|
||||
{ "LogPath", "Log Path", "/log.txt" },
|
||||
{ "LogDebug","Log Debug Messages", false }
|
||||
}
|
||||
|
||||
-- load data from the settings file
|
||||
---@param target pkt_config
|
||||
---@param raw boolean? true to not use default values
|
||||
local function load_settings(target, raw)
|
||||
for _, v in pairs(fields) do settings.unset(v[1]) end
|
||||
|
||||
local loaded = settings.load("/pocket.settings")
|
||||
|
||||
for _, v in pairs(fields) do target[v[1]] = settings.get(v[1], tri(raw, nil, v[3])) end
|
||||
|
||||
return loaded
|
||||
end
|
||||
|
||||
-- create the config view
|
||||
---@param display graphics_element
|
||||
local function config_view(display)
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local function exit() os.queueEvent("terminate") end
|
||||
|
||||
TextBox{parent=display,y=1,text="Pocket Configurator",alignment=CENTER,height=1,fg_bg=style.header}
|
||||
|
||||
local root_pane_div = Div{parent=display,x=1,y=2}
|
||||
|
||||
local main_page = Div{parent=root_pane_div,x=1,y=1}
|
||||
local net_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local log_cfg = Div{parent=root_pane_div,x=1,y=1}
|
||||
local summary = Div{parent=root_pane_div,x=1,y=1}
|
||||
local changelog = Div{parent=root_pane_div,x=1,y=1}
|
||||
|
||||
local main_pane = MultiPane{parent=root_pane_div,x=1,y=1,panes={main_page,net_cfg,log_cfg,summary,changelog}}
|
||||
|
||||
-- Main Page
|
||||
|
||||
local y_start = 7
|
||||
|
||||
TextBox{parent=main_page,x=2,y=2,height=4,text="Welcome to the Pocket configurator! Please select one of the following options."}
|
||||
|
||||
if tool_ctl.ask_config then
|
||||
TextBox{parent=main_page,x=2,y=y_start,height=4,width=49,text="Please configure before starting up.",fg_bg=cpair(colors.red,colors.lightGray)}
|
||||
y_start = y_start + 3
|
||||
end
|
||||
|
||||
local function view_config()
|
||||
tool_ctl.viewing_config = true
|
||||
tool_ctl.gen_summary(settings_cfg)
|
||||
tool_ctl.settings_apply.hide(true)
|
||||
main_pane.set_value(4)
|
||||
end
|
||||
|
||||
if fs.exists("/pocket/config.lua") then
|
||||
PushButton{parent=main_page,x=2,y=y_start,min_width=22,text="Import Legacy Config",callback=function()tool_ctl.load_legacy()end,fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=btn_act_fg_bg}
|
||||
y_start = y_start + 2
|
||||
end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=y_start,min_width=18,text="Configure Device",callback=function()main_pane.set_value(2)end,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.view_cfg = PushButton{parent=main_page,x=2,y=y_start+2,min_width=20,text="View Configuration",callback=view_config,fg_bg=cpair(colors.black,colors.blue),active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
|
||||
|
||||
if not tool_ctl.has_config then tool_ctl.view_cfg.disable() end
|
||||
|
||||
PushButton{parent=main_page,x=2,y=18,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=main_page,x=14,y=18,min_width=12,text="Change Log",callback=function()main_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#region Network
|
||||
|
||||
local net_c_1 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
local net_c_2 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
local net_c_3 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
local net_c_4 = Div{parent=net_cfg,x=2,y=4,width=24}
|
||||
|
||||
local net_pane = MultiPane{parent=net_cfg,x=1,y=4,panes={net_c_1,net_c_2,net_c_3,net_c_4}}
|
||||
|
||||
TextBox{parent=net_cfg,x=1,y=2,height=1,text=" Network Configuration",fg_bg=cpair(colors.black,colors.lightBlue)}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=1,height=1,text="Set network channels."}
|
||||
TextBox{parent=net_c_1,x=1,y=3,height=4,text="Each of the named channels must be the same within a particular SCADA network.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=8,height=1,width=18,text="Supervisor Channel"}
|
||||
local svr_chan = NumberField{parent=net_c_1,x=1,y=9,width=7,default=ini_cfg.SVR_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=9,height=4,text="[SVR_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=10,height=1,width=19,text="Coordinator Channel"}
|
||||
local crd_chan = NumberField{parent=net_c_1,x=1,y=11,width=7,default=ini_cfg.CRD_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=11,height=4,text="[CRD_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_1,x=1,y=12,height=1,width=14,text="Pocket Channel"}
|
||||
local pkt_chan = NumberField{parent=net_c_1,x=1,y=13,width=7,default=ini_cfg.PKT_Channel,min=1,max=65535,fg_bg=bw_fg_bg}
|
||||
TextBox{parent=net_c_1,x=9,y=13,height=4,text="[PKT_CHANNEL]",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local chan_err = TextBox{parent=net_c_1,x=1,y=14,height=1,width=24,text="Please set all channels.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_channels()
|
||||
local svr_c, crd_c, pkt_c = tonumber(svr_chan.get_value()), tonumber(crd_chan.get_value()), tonumber(pkt_chan.get_value())
|
||||
if svr_c ~= nil and crd_c ~= nil and pkt_c ~= nil then
|
||||
tmp_cfg.SVR_Channel, tmp_cfg.CRD_Channel, tmp_cfg.PKT_Channel = svr_c, crd_c, pkt_c
|
||||
net_pane.set_value(2)
|
||||
chan_err.hide(true)
|
||||
else chan_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_1,x=19,y=15,text="Next \x1a",callback=submit_channels,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=1,height=1,text="Set connection timeout."}
|
||||
TextBox{parent=net_c_2,x=1,y=3,height=7,text="You generally should not need to modify this. On slow servers, you can try to increase this to make the system wait longer before assuming a disconnection.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=1,y=11,height=1,width=19,text="Connection Timeout"}
|
||||
local timeout = NumberField{parent=net_c_2,x=1,y=12,width=7,default=ini_cfg.ConnTimeout,min=2,max=25,max_chars=6,max_frac_digits=2,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_2,x=9,y=12,height=2,text="seconds\n(default 5)",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local ct_err = TextBox{parent=net_c_2,x=1,y=14,height=1,width=24,text="Please set timeout.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_timeouts()
|
||||
local timeout_val = tonumber(timeout.get_value())
|
||||
if timeout_val ~= nil then
|
||||
tmp_cfg.ConnTimeout = timeout_val
|
||||
net_pane.set_value(3)
|
||||
ct_err.hide(true)
|
||||
else ct_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_2,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_2,x=19,y=15,text="Next \x1a",callback=submit_timeouts,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_3,x=1,y=1,height=1,text="Set the trusted range."}
|
||||
TextBox{parent=net_c_3,x=1,y=3,height=4,text="Setting this to a value larger than 0 prevents connections with devices that many blocks away.",fg_bg=g_lg_fg_bg}
|
||||
TextBox{parent=net_c_3,x=1,y=8,height=4,text="This is optional. You can disable this functionality by setting the value to 0.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local range = NumberField{parent=net_c_3,x=1,y=13,width=10,default=ini_cfg.TrustedRange,min=0,max_chars=20,allow_decimal=true,fg_bg=bw_fg_bg}
|
||||
|
||||
local tr_err = TextBox{parent=net_c_3,x=1,y=14,height=1,width=24,text="Set the trusted range.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_tr()
|
||||
local range_val = tonumber(range.get_value())
|
||||
if range_val ~= nil then
|
||||
tmp_cfg.TrustedRange = range_val
|
||||
net_pane.set_value(4)
|
||||
tr_err.hide(true)
|
||||
else tr_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_3,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=net_c_3,x=19,y=15,text="Next \x1a",callback=submit_tr,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=1,height=4,text="Optionally, set the facility authentication key. Do NOT use one of your passwords."}
|
||||
TextBox{parent=net_c_4,x=1,y=6,height=6,text="This enables verifying that messages are authentic, so it is intended for security on multiplayer servers.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
TextBox{parent=net_c_4,x=1,y=12,height=1,text="Facility Auth Key"}
|
||||
local key, _, censor = TextField{parent=net_c_4,x=1,y=13,max_len=64,value=ini_cfg.AuthKey,width=24,height=1,fg_bg=bw_fg_bg}
|
||||
|
||||
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
|
||||
|
||||
-- declare back first so tabbing makes sense visually
|
||||
PushButton{parent=net_c_4,x=1,y=15,text="\x1b Back",callback=function()net_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
local hide_key = CheckBox{parent=net_c_4,x=8,y=15,label="Hide Key",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
|
||||
|
||||
hide_key.set_value(true)
|
||||
censor_key(true)
|
||||
|
||||
local key_err = TextBox{parent=net_c_4,x=1,y=14,height=1,width=24,text="Length must be > 7.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_auth()
|
||||
local v = key.get_value()
|
||||
if string.len(v) == 0 or string.len(v) >= 8 then
|
||||
tmp_cfg.AuthKey = key.get_value()
|
||||
main_pane.set_value(3)
|
||||
key_err.hide(true)
|
||||
else key_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=net_c_4,x=19,y=15,text="Next \x1a",callback=submit_auth,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Logging
|
||||
|
||||
local log_c_1 = Div{parent=log_cfg,x=2,y=4,width=24}
|
||||
|
||||
TextBox{parent=log_cfg,x=1,y=2,height=1,text=" Logging Configuration",fg_bg=cpair(colors.black,colors.pink)}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=1,height=1,text="Configure logging below."}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=3,height=1,text="Log File Mode"}
|
||||
local mode = RadioButton{parent=log_c_1,x=1,y=4,default=ini_cfg.LogMode+1,options={"Append on Startup","Replace on Startup"},callback=function()end,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.pink}
|
||||
|
||||
TextBox{parent=log_c_1,x=1,y=7,height=1,text="Log File Path"}
|
||||
local path = TextField{parent=log_c_1,x=1,y=8,width=24,height=1,value=ini_cfg.LogPath,max_len=128,fg_bg=bw_fg_bg}
|
||||
|
||||
local en_dbg = CheckBox{parent=log_c_1,x=1,y=10,default=ini_cfg.LogDebug,label="Enable Debug Messages",box_fg_bg=cpair(colors.pink,colors.black)}
|
||||
TextBox{parent=log_c_1,x=3,y=11,height=4,text="This results in much larger log files. Use only as needed.",fg_bg=g_lg_fg_bg}
|
||||
|
||||
local path_err = TextBox{parent=log_c_1,x=1,y=14,height=1,width=24,text="Provide a log file path.",fg_bg=cpair(colors.red,colors.lightGray),hidden=true}
|
||||
|
||||
local function submit_log()
|
||||
if path.get_value() ~= "" then
|
||||
path_err.hide(true)
|
||||
tmp_cfg.LogMode = mode.get_value() - 1
|
||||
tmp_cfg.LogPath = path.get_value()
|
||||
tmp_cfg.LogDebug = en_dbg.get_value()
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
tool_ctl.viewing_config = false
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
main_pane.set_value(4)
|
||||
else path_err.show() end
|
||||
end
|
||||
|
||||
PushButton{parent=log_c_1,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(2)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=log_c_1,x=19,y=15,text="Next \x1a",callback=submit_log,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
--#endregion
|
||||
|
||||
--#region Summary and Saving
|
||||
|
||||
local sum_c_1 = Div{parent=summary,x=2,y=4,width=24}
|
||||
local sum_c_2 = Div{parent=summary,x=2,y=4,width=24}
|
||||
local sum_c_3 = Div{parent=summary,x=2,y=4,width=24}
|
||||
local sum_c_4 = Div{parent=summary,x=2,y=4,width=24}
|
||||
|
||||
local sum_pane = MultiPane{parent=summary,x=1,y=4,panes={sum_c_1,sum_c_2,sum_c_3,sum_c_4}}
|
||||
|
||||
TextBox{parent=summary,x=1,y=2,height=1,text=" Summary",fg_bg=cpair(colors.black,colors.green)}
|
||||
|
||||
local setting_list = ListBox{parent=sum_c_1,x=1,y=1,height=11,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
local function back_from_summary()
|
||||
if tool_ctl.viewing_config or tool_ctl.importing_legacy then
|
||||
main_pane.set_value(1)
|
||||
tool_ctl.viewing_config = false
|
||||
tool_ctl.importing_legacy = false
|
||||
tool_ctl.settings_apply.show()
|
||||
else
|
||||
main_pane.set_value(3)
|
||||
end
|
||||
end
|
||||
|
||||
---@param element graphics_element
|
||||
---@param data any
|
||||
local function try_set(element, data)
|
||||
if data ~= nil then element.set_value(data) end
|
||||
end
|
||||
|
||||
local function save_and_continue()
|
||||
for k, v in pairs(tmp_cfg) do settings.set(k, v) end
|
||||
|
||||
if settings.save("/pocket.settings") then
|
||||
load_settings(settings_cfg, true)
|
||||
load_settings(ini_cfg)
|
||||
|
||||
try_set(svr_chan, ini_cfg.SVR_Channel)
|
||||
try_set(crd_chan, ini_cfg.CRD_Channel)
|
||||
try_set(pkt_chan, ini_cfg.PKT_Channel)
|
||||
try_set(timeout, ini_cfg.ConnTimeout)
|
||||
try_set(range, ini_cfg.TrustedRange)
|
||||
try_set(key, ini_cfg.AuthKey)
|
||||
try_set(mode, ini_cfg.LogMode)
|
||||
try_set(path, ini_cfg.LogPath)
|
||||
try_set(en_dbg, ini_cfg.LogDebug)
|
||||
|
||||
tool_ctl.view_cfg.enable()
|
||||
|
||||
if tool_ctl.importing_legacy then
|
||||
tool_ctl.importing_legacy = false
|
||||
sum_pane.set_value(3)
|
||||
else
|
||||
sum_pane.set_value(2)
|
||||
end
|
||||
else
|
||||
sum_pane.set_value(4)
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_1,x=1,y=15,text="\x1b Back",callback=back_from_summary,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
tool_ctl.show_key_btn = PushButton{parent=sum_c_1,x=1,y=13,min_width=17,text="Unhide Auth Key",callback=function()tool_ctl.show_auth_key()end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg,dis_fg_bg=dis_fg_bg}
|
||||
tool_ctl.settings_apply = PushButton{parent=sum_c_1,x=18,y=15,min_width=7,text="Apply",callback=save_and_continue,fg_bg=cpair(colors.black,colors.green),active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
TextBox{parent=sum_c_2,x=1,y=1,height=1,text="Settings saved!"}
|
||||
|
||||
local function go_home()
|
||||
main_pane.set_value(1)
|
||||
net_pane.set_value(1)
|
||||
sum_pane.set_value(1)
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_2,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_2,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_3,x=1,y=1,height=4,text="The old config.lua file will now be deleted, then the configurator will exit."}
|
||||
|
||||
local function delete_legacy()
|
||||
fs.delete("/pocket/config.lua")
|
||||
exit()
|
||||
end
|
||||
|
||||
PushButton{parent=sum_c_3,x=1,y=15,min_width=8,text="Cancel",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_3,x=19,y=15,min_width=6,text="OK",callback=delete_legacy,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
TextBox{parent=sum_c_4,x=1,y=1,height=8,text="Failed to save the settings file.\n\nThere may not be enough space for the modification or server file permissions may be denying writes."}
|
||||
PushButton{parent=sum_c_4,x=1,y=15,min_width=6,text="Home",callback=go_home,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
PushButton{parent=sum_c_4,x=19,y=15,min_width=6,text="Exit",callback=exit,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.white,colors.gray)}
|
||||
|
||||
--#endregion
|
||||
|
||||
-- Config Change Log
|
||||
|
||||
local cl = Div{parent=changelog,x=2,y=4,width=24}
|
||||
|
||||
TextBox{parent=changelog,x=1,y=2,height=1,text=" Config Change Log",fg_bg=bw_fg_bg}
|
||||
|
||||
local c_log = ListBox{parent=cl,x=1,y=1,height=13,width=24,scroll_height=100,fg_bg=bw_fg_bg,nav_fg_bg=g_lg_fg_bg,nav_active=cpair(colors.black,colors.gray)}
|
||||
|
||||
for _, change in ipairs(changes) do
|
||||
TextBox{parent=c_log,text=change[1],height=1,fg_bg=bw_fg_bg}
|
||||
for _, v in ipairs(change[2]) do
|
||||
local e = Div{parent=c_log,height=#util.strwrap(v,21)}
|
||||
TextBox{parent=e,y=1,x=1,text="- ",height=1,fg_bg=cpair(colors.gray,colors.white)}
|
||||
TextBox{parent=e,y=1,x=3,text=v,height=e.get_height(),fg_bg=cpair(colors.gray,colors.white)}
|
||||
end
|
||||
end
|
||||
|
||||
PushButton{parent=cl,x=1,y=15,text="\x1b Back",callback=function()main_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
|
||||
|
||||
-- set tool functions now that we have the elements
|
||||
|
||||
-- load a legacy config file
|
||||
function tool_ctl.load_legacy()
|
||||
local config = require("pocket.config")
|
||||
|
||||
tmp_cfg.SVR_Channel = config.SVR_CHANNEL
|
||||
tmp_cfg.CRD_Channel = config.CRD_CHANNEL
|
||||
tmp_cfg.PKT_Channel = config.PKT_CHANNEL
|
||||
tmp_cfg.ConnTimeout = config.COMMS_TIMEOUT
|
||||
tmp_cfg.TrustedRange = config.TRUSTED_RANGE
|
||||
tmp_cfg.AuthKey = config.AUTH_KEY or ""
|
||||
|
||||
tmp_cfg.LogMode = config.LOG_MODE
|
||||
tmp_cfg.LogPath = config.LOG_PATH
|
||||
tmp_cfg.LogDebug = config.LOG_DEBUG or false
|
||||
|
||||
tool_ctl.gen_summary(tmp_cfg)
|
||||
sum_pane.set_value(1)
|
||||
main_pane.set_value(4)
|
||||
tool_ctl.importing_legacy = true
|
||||
end
|
||||
|
||||
-- expose the auth key on the summary page
|
||||
function tool_ctl.show_auth_key()
|
||||
tool_ctl.show_key_btn.disable()
|
||||
tool_ctl.auth_key_textbox.set_value(tool_ctl.auth_key_value)
|
||||
end
|
||||
|
||||
-- generate the summary list
|
||||
---@param cfg pkt_config
|
||||
function tool_ctl.gen_summary(cfg)
|
||||
setting_list.remove_all()
|
||||
|
||||
local alternate = false
|
||||
local inner_width = setting_list.get_width() - 1
|
||||
|
||||
tool_ctl.show_key_btn.enable()
|
||||
tool_ctl.auth_key_value = cfg.AuthKey or "" -- to show auth key
|
||||
|
||||
for i = 1, #fields do
|
||||
local f = fields[i]
|
||||
local height = 1
|
||||
local label_w = string.len(f[2])
|
||||
local val_max_w = (inner_width - label_w) - 1
|
||||
local raw = cfg[f[1]]
|
||||
local val = util.strval(raw)
|
||||
|
||||
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
|
||||
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace") end
|
||||
|
||||
if val == "nil" then val = "<not set>" end
|
||||
|
||||
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
|
||||
alternate = not alternate
|
||||
|
||||
if string.len(val) > val_max_w then
|
||||
local lines = util.strwrap(val, inner_width)
|
||||
height = #lines + 1
|
||||
end
|
||||
|
||||
local line = Div{parent=setting_list,height=height,fg_bg=c}
|
||||
TextBox{parent=line,text=f[2],width=string.len(f[2]),fg_bg=cpair(colors.black,line.get_fg_bg().bkg)}
|
||||
|
||||
local textbox
|
||||
if height > 1 then
|
||||
textbox = TextBox{parent=line,x=1,y=2,text=val,height=height-1,alignment=LEFT}
|
||||
else
|
||||
textbox = TextBox{parent=line,x=label_w+1,y=1,text=val,alignment=RIGHT}
|
||||
end
|
||||
|
||||
if f[1] == "AuthKey" then tool_ctl.auth_key_textbox = textbox end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- reset terminal screen
|
||||
local function reset_term()
|
||||
term.setTextColor(colors.white)
|
||||
term.setBackgroundColor(colors.black)
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
end
|
||||
|
||||
-- run the pcoket configurator
|
||||
---@param ask_config? boolean indicate if this is being called by the startup app due to an invalid configuration
|
||||
function configurator.configure(ask_config)
|
||||
tool_ctl.ask_config = ask_config == true
|
||||
|
||||
load_settings(settings_cfg, true)
|
||||
tool_ctl.has_config = load_settings(ini_cfg)
|
||||
|
||||
reset_term()
|
||||
|
||||
-- set overridden colors
|
||||
for i = 1, #style.colors do
|
||||
term.setPaletteColor(style.colors[i].c, style.colors[i].hex)
|
||||
end
|
||||
|
||||
local status, error = pcall(function ()
|
||||
local display = DisplayBox{window=term.current(),fg_bg=style.root}
|
||||
config_view(display)
|
||||
|
||||
while true do
|
||||
local event, param1, param2, param3 = util.pull_event()
|
||||
|
||||
-- handle event
|
||||
if event == "timer" then
|
||||
tcd.handle(param1)
|
||||
elseif event == "mouse_click" or event == "mouse_up" or event == "mouse_drag" or event == "mouse_scroll" or event == "double_click" then
|
||||
local m_e = core.events.new_mouse_event(event, param1, param2, param3)
|
||||
if m_e then display.handle_mouse(m_e) end
|
||||
elseif event == "char" or event == "key" or event == "key_up" then
|
||||
local k_e = core.events.new_key_event(event, param1, param2)
|
||||
if k_e then display.handle_key(k_e) end
|
||||
elseif event == "paste" then
|
||||
display.handle_paste(param1)
|
||||
end
|
||||
|
||||
if event == "terminate" then return end
|
||||
end
|
||||
end)
|
||||
|
||||
-- restore colors
|
||||
for i = 1, #style.colors do
|
||||
local r, g, b = term.nativePaletteColor(style.colors[i].c)
|
||||
term.setPaletteColor(style.colors[i].c, r, g, b)
|
||||
end
|
||||
|
||||
reset_term()
|
||||
if not status then
|
||||
println("configurator error: " .. error)
|
||||
end
|
||||
|
||||
return status, error
|
||||
end
|
||||
|
||||
return configurator
|
||||
@@ -1,35 +0,0 @@
|
||||
--
|
||||
-- Core I/O - Pocket Central I/O Management
|
||||
--
|
||||
|
||||
local psil = require("scada-common.psil")
|
||||
|
||||
local coreio = {}
|
||||
|
||||
---@class pocket_core_io
|
||||
local io = {
|
||||
ps = psil.create()
|
||||
}
|
||||
|
||||
---@enum POCKET_LINK_STATE
|
||||
local LINK_STATE = {
|
||||
UNLINKED = 0,
|
||||
SV_LINK_ONLY = 1,
|
||||
API_LINK_ONLY = 2,
|
||||
LINKED = 3
|
||||
}
|
||||
|
||||
coreio.LINK_STATE = LINK_STATE
|
||||
|
||||
-- get the core PSIL
|
||||
function coreio.core_ps()
|
||||
return io.ps
|
||||
end
|
||||
|
||||
-- set network link state
|
||||
---@param state POCKET_LINK_STATE
|
||||
function coreio.report_link_state(state)
|
||||
io.ps.publish("link_state", state)
|
||||
end
|
||||
|
||||
return coreio
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user