From 451ead1f6fa23c02883b2f43da3ddb841e73b354 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 07:52:25 +0700 Subject: [PATCH 01/23] staging: add validated SCL export source transformer --- .staging/apply_scl_export.py.zlib.b64 | 1 + 1 file changed, 1 insertion(+) create mode 100644 .staging/apply_scl_export.py.zlib.b64 diff --git a/.staging/apply_scl_export.py.zlib.b64 b/.staging/apply_scl_export.py.zlib.b64 new file mode 100644 index 00000000..8c523e16 --- /dev/null +++ b/.staging/apply_scl_export.py.zlib.b64 @@ -0,0 +1 @@ +eNrtPNly20iS7/yKaniiTXogUJRtWdblkCiqzVnJcogaeaPbHW4IKJK1AgEOCtTRakbM03zAxn7Bftp+yWbWARQuirLsae9suztssI6srLyzrmEcTcjUTcYBuyBsMo3ihLyHn41Gw6dDElPXb2L1JuFJ3CIru/jvZoPAn5gmszgUrUWTloOtPyX0JmnS0It8Fo52rFkyXNmwgredcwSmgG0CbbOYL+LQiqBG1CnbkzDxJlc+ixuyh985yyeUZvQG8aTT9Gl+NkqdhSDSXzwL2hewMomIb0OWEh3rI9himNMp4Hr0U9R6NFmip9NosBXX9BLfQXuBQ0qaMOG2IiwUE5QlBk0w0JR5kWzMCE7osARP5owTEvDkNXf7ZCOAcJlnJLBLU/opHfDkubQuhNozDeBHlPqJdQnQEfCo1nsUXIRRN6lTYYAyid3AuLcajUK2Dhq1ji8mKBNOkiQJ+SAXjEA47mxT/7adxo37iQAjIVkWMcuCz+w0I+uHSwHuGm1QUQxGFbY4uvp06fkM/5snwGe+zgbgl871t0+jAzsJO/cCZ1b5DAKkwH7FbjZeS5/faBsNIaWAzph+zAzq0Ee9+cwiukoRlLC8IPETZh3ShWh++ElIIGoncVsMhFy1h27sQsciXtBwKaccau924D5/0EIixy78YiFO9aqvWqvbdirOdJY/d6BlLmEJQElE9EYdRSw/wSQWBRykLPPJeH+LEmikAyS24BWzID6fS8KZSOcizvKqAy/uwHzLncsaNaNwiEbzWI6YKPQDfgnUfU48vZ5L3QvAuobnO26Yc9niRoFUYqi4IxNzSYaFVU1t3Y/G43tc0avL6Ib8oH5yRjkaN0ib5UMwffuNuBz5XJdvfYiq4bv3UdNfxtNODlwE3fHOu6s2Wur5GitA/9YwK44ugR+PVl7uf68t68LzsZA85ByDqPrMuBonByBbe+6QKRTlFRd1Qv9QkX7S2K87ry0nzsvyd6ag9jDX2QV/uuQzmuogVpy9MruvCZHz8XUXtiddfLjg+aG2P8lQt1R8zpkQbBjncVuyKWDfMyMttuSubvbbSUFnwdruy315zN7fykNPaWT6Ip+Lb3s8z1hi1RNpWJKDP6ltHLDXidH63Zngxyv4ycUdDYMGV5/8erFxjehn19emsHlNCBA+sMJ/eGE/nBCfzihpTR04F7RgRd8RcVUI1Qqpar7V1LJl/YL8rbzCpRy1X5FzkGG374kP5LjDSg/fw1V6+T8BTl+jdJ93nkhC5bT3o6zUSvjn6HW37peoDurStpFvk6MbEssC2Q/sgyNg3gRWY4Jmk8DmtBPkLFd0vjh3nG7/UPM/N1G42GKGsU+jHaziSnwjrU/47cnVzQO3FuLnDPOLljAklvIQ6MgcKecAl/2Xe9Sp7BPDtcOXx0eHO6B0kZxSONT12czFIY1S1BITUpRaELFqs0/Z2bfTBz8heLfzxpbG5SXhkF5aa4hrLzA/1c/D/rbKGa/RmHiBnsBBDfIXlBfHAakh8ZA8FzNWTS1yHs3pIHzYz/06Y2Qk4crYsmaGuYSvr+KNf2/Fsc/1uBlhu7bMij1JjdnP9OfhuUB25tE05UYJQEAoHJl5leusJcWZm2x/ipXdLtgtFei65D6ZDgLAuIz7gGM+JZMIhiNuKGy6RxsDXUaopRna77iZ/s4ClkSxfKX43EYmw2JNeOo8nunTp96652Nl6vOgQa/ZZEwSnBRXEKUa9opdPmRLkSn7KoACcGM8yGKL8GhenTrY2jZi1pnCHwMHwSr0zLnXlzQllVquRL/msbsCghGACLY2xTmG/KJe0HFEA/rZDY+YlcUWgvap7M7iLwZigd0DaA+LRetjFGxssTzIaOBj+KjR/w0jaMpmL7bzM9NZxfgPMqYDgxExRB3KQVHFPzkbmEuaS2nSfp9l1NPkKTmKR1SCJc82vvbDNLNZg6ITa7cYEZbLbWJsZXrnmsKExBt800Aekz5uBtNprOE+s1WVj1vyL+Fni4pAGW61ZWTPxfoeQ8zj0q8rKFxBdM/l9JlUPfQu9zhUVRfJKmajtbSypkn9wXEIZgwQQjluSFS7rs+R/NOvv8eP5VdAzMhcVPdeBLLTMsbo89LKO5vYO+3LgcFOKAcwgPJnuL+zuMG1t1UElfo1zSVD3cKQ7Tov/1WITa6tlU1r3yKiINUAU6Z9Yb8ycIu5E5sOZEhbiMnY0qAN+hXBt0jcFxIEsmJLCzbXIBZTlRqhwhcnhA+8zwIRMB/ESElx8eDTFLyQeAmsigdUDCwFArgYKezEORzRTBnGMV63CSCLGcKMg9OlngguOiO5aBSIC8otKboMQUhu0flGHQTwJ8AZQRdAHQeTCbhxrA5qNajRdESDh2H95TuSe/+YC0SWoxb0M0Q8IyGzVSiW4YyF1qYiGKzqh3QBwPVGlHbJC/Tn4NeRrXUekMEw4aQD4gs2Ii3aoIiW9FQRl7voFuFSaM304gLAYPIK56FCZuAJkGAFvMxmzqNUHbTAdiAxrglz9sSnI5kILPDcxIiEjN6FHkqq6oMlAYkd/z7PkCDEJccgKDioYK04A5dzlYajHD8MYexQnrdrDQuKQTqo3CjqBaBOqpuq856fhvIPcBtg5XCJsoZKv+s3HImYIpRhjnRwpDJ1r38thVTQci09JRkRfdSEnoq20lpMTsVxUXVVR5YuHLjVJbBoOwQ99plYJvBMIN2OApBTRy1bbDHb0OvCartUcicUI3OIFcMbVSxEQQHvOWkmwh7CK85hF60lQ8kcGguAcK4GRK1hyt+R1wxxCqMk5cO7ZhLfsMXwu4cVUVXiyBuNR5ILEuLgCGIEOih+JtZ5RLSZGuJkRbvPY1XxCIhinWciHh4GEBW2phAhlp3jmhROon5Wk8AM/JJ6K6ySQkV//kKmaQx9sNTycYYzHtgLolqI3UVMZ8Udwma0cV/gBUCPoc+rgOcRui1e1fAk714xIk675ZF8yhm353Ftz/QRBrJQ4iY5OJHUwMBGFITRIMWBopKxmRImRdBHeo3TEGaKPlT/cww8Y1jxB7kzZt6+d3KYa1AFrUgn6gMaIJOesabCupyUd2dai2iqk0UzjTIe3hk51SHdoUxoiUivQrQBatRzLPmGRsGNBD+SR3N07zYyvFJLhADQ7yYTVGNq3lWE3uLcHVhIC8nbx3dH40X8AJOTdwD5gbRSPplnXxIA9A0iGmXZ1EnHicYLuFJyjHjGc3ycmYO7QzG0bX8bLaQCEl6hHSh+EsYaH9NYLg0KQTb0bwZiOr3cTRkQSVnBrPhkN0sZgoygoUJFcEnLrBbSPLMgBdIS32GJEohSxRBtXuyYg3hUX9NQKF+p9DdL/LkEFCXE6wh+pk4m7eTZmqmHpD/+ft/kTuFAigq2OJbeT7RzsGAQRLBuRK2FfpsqTqyRvr9A4K0Jc1nDmN+6zfxz297QSCKOZY/w9JnVUqbAuqQftcA5ElAXjWgPOYHdOjOgqSHyZfGPiujIbrnfI89309riBS5EjGojELRrAzcIeoFFg4SOjE1ozVfuTPlCH7muD+/q0HI2H8paIdf0gvUpWV1I4lvF6wwLZLx778v8acsuBWNvpMBPLR6B2BO4g9jiFEGCNoYbiBohHserSoYSFrw6IwnvLpTftZ3JQAo99JUQr89/gN8xczTaCuYtlItR7O3YOqrzH3B5Ouxzm6nKufYw1w/HWAiF+lK5tJWtFyEwjz9grTWG5Nm78aj0mXQmxa5HoM7a9Ibwjjpn2R14N364ZUbMP8ErZPYBTQrIUoRuVBaWGe6QSuOQNqs3unpySkEkBY6HRlmwa+Se72jN84xeBt3BLakQMljN74E4R2FEU+YtwfRVtIsNMniiBJkHFfFqUMX6OQ7JAPGcf5iU8Yn1ywZk+9KLlthtR/dCC0S+oOXFDS6ODW1oAATy1rLIM05+TezsD+Bf51eHEfx0rz6MgSehek9AoMaECYJ2v9/pblSxlzMfo/2F7IkFa5plVXrDxCST2eJMDaFgB59MiSZYL3BHfTNMABGg7wTQgAocz5gboZqzZuNiuQxZ8/y3iYbOl+O/r96PPQgJ0Lc+D2GMV1OIcuYadW6RdKcQXTeJKUmdmmk90AjyEspOGxogxmuSMLLDXEhsI+pv7V32u91BWvyscG8ZXg0pTW5Blb/3eFJIQzIaVOupiTsuJzok5EUE5KL7EghssHV7mwx3SFHBzt3UhgcwAoPJEhx6ooLPTY5elesfwcuQdfifj8oYdpE/dbVp939tOqU4kyA3UkcBbrBUB7l4GmrQ1XgyBYOIplWnqRyJYxFShNMeVw0WijZCiSm70WYZ+4lbXbWWnX2DLsH9EqkoQoMxN0gnrhWp/awLKHFoPIDIX644+TGjKPKx9DBDfpgY2LadTltVQaawlZWRI61A35w4xDX6pcaEof4sHf6ToTiQqjK61ux3pZbqEQan3QTr2o6qntvMk1uK1NY8tNdCdD85wJSSiME6e/xIhD9/zdJQXZBFOd36XzQieuqKk8+z8WllRJHdgkISKWXE2S9P4qoBrsCYOfE9aUyuoGppOLEh+Z/EyJjF1cOrsBrCQUGQc7rkFCAnMOqms5eeKt5iMvEX0KeC1FrjYNuLOGdlzFV0qglEalR/3QYcM/ojDDZl+sFBduagW1WjwujtQomtjRkZXXGj8rqwWwCQcat8MFbC519MfxuZO5ugavPXErVqRRn4U6CiuyznXwZNZQD/YzOQGNz8UE3ivTiRQnYkiGIwhPgS7VaEHsUkL4v2ChCXi7EUPNDh84hU17xojCk8hxU2fnnSJLl66qg3L4/BXuCS/zZCk1a9DvEDFq3UuaqDfG7kiA4Z9FRdE1jzM1i5oag6/PfO3y4lq4xCx+Ur6wOH2CO98QOCp4RO6QAl4kdlnOsapDHO9YSoDrHupTrkp5Vw6zwrLpqac+aZ8YX86wFsCXPqneEZLMH+dNlXFeFxpQ8lSlpC9xUEZJN8v3tCj/zQN9SHlQbejVi+lONqH7Hxoi6iTFiwZwXk+Ucl0XmXLBFyGlJtWyJP7h1Pn4M4X9JjE29yDvHou6R+C3JCgW9K+ajYAp+pEzElhJLbJ3hOy+ZyTR1r0n6szS+poFM6fsh2I+JWKPKuIDH+3CPceE2XOUeo7rZkDvA6eJecQrDPG1//07ex9DSTz6InUv5EoQcp3Xf+w/ykIreXGQhxwPrEEZNI4ieQL24mIF8/4HeuF4S3BLc31ePQFTtmOqzxxron4kuEfuYNJhWbWNycTUhO1GWX7yWxW/UOcKKOEPdaFhkkmVfzJ36vQNMnUSBg88MmEtOCI3JpUkAh4Ll/EATtVipF0DxRQLebIG7fOvyMRiSIoSLGQt8MU+xHyKQ2pdlTYmrc0TDUarXJS/l6TcPUHxkj7xdVSM4e1NI9P2mQhmPFCTADd5MAeCUn356ChPOigr7UmmUpoGeRRJlnKKgj/znqfPUwFe9AFJPcQnWJLksqVWihUKwtDIpKOLsY82WdkGpcGd7URD+SB3DIBHPixEl/F9WzSTMnJZVn+NXR8qgUB6v0CmTN47EVOUaf3pcw9xX1fcAgGHbsoTcbHYDF29z7MXAXEG6M8oxrs/1zFZBbiZBCM3HSTLdbLel2efOhHlxxKMhvmAzaV+zcHjTXltdXW/jiO0pLs+FibC9BUibNw+DlXUXG487mXvI7sW8XrX0fZmXq/B9zMJc3THSM61uZPeLcKbiMstsehTJ8307VpdiAir2li1ySjn7VRyw2bHeRfJXBsG8/VG8pbU/Mrbd8D2WQ3fCglsgPKRlHHJ6Oooo+WufnGPIjsHPmXiwSJfbpAuW4SJmGRS89QX9OacJb7vT6QqDAgf+UveCtvGuWXo/Cu9mpl1FlXMaXR/QIQuFAPH8pZrtXGVK3GfFCzw17fZmSWQ2lRffSiM2ipdtau644X2VOV648uXjTZ2Nwt2nbXmvrnSH5955PnAe33yXGkJXE6YbBbNJeD9tiu20er1er8W6rsuzBUhXoNNY8PKR0Qc8VShupMnXkIRRyC8cCRNhvIq0UfMq0v3PGrWXwArID2NodITbzO4pdtbtVXu18lqhNDaLkTjG0HFeScYaLAxCGVhlFzjltsr8wTh+XfJl3BS9c9it/TMpuHYvBQfFFZj5F8Q2e0vrQwxWXtg//Fqeks81wipWyCH34muRElp6l+KKbB6VIi1LuNRalMlFBOkc6UNIxiXJTR6Yq3v83jvG+pAWAjOhVJ3duheYOtt0TCcXNMZkY8cyjjvV2siMVxkNNiQ7lqC5ac06zstKEbnvwmtOLMga7rXLXB9i2X7/wCHZGalcVReq9shfBifvCFXpvVqHEBcYcLsBYqlQL0SIZR2MSBMakguImnwq7vHg2gIS2Kl2CpkEFZx9O+/tt9syejDjiUrpA2k7iZkORXes7NK3tfAC+D0iqi/oY9YmNUccF7fqopkBBLfpdf0+l63xfYgZNaNT8K65F/LUA3kLR87C4Zqx30MGCBwxhlfHw8rjdzpr6esBCFethuSiuiKLNGu22zKcFo9LQKJSm47IdAYaybPUMuFyZAO+1Vh4ALvRwHxMHiUr5i9QqS95UDcAsfUwz9HnTOXptvTIZuPOvNZR3aZZ3DJhxdMID9+XAdykuck2JYpNHH2U5Px5p7hksswBC5Ydq9DpuyoqrZkMKo4L1wNecCxX7Z/3e10iOFJ9brhi26KIUM6kk5204qf80aICyTgu9TQXEtJ+BIDOeWc9m+vPW6Uz2cV9pmrwBdYXVoZz95M0n9XFpnnV5c0S76oa90+p65+Ewe0R48l2/S7hbpH0VcAWbDJWU0IBkfexGvNUQaeQcYOu5TVUrU9sqrS8UkPVafFvUDExf4F0/VeKm/RRSMPc0iLunQmLLY4NG8ffiyZHz6TyUOcS8lMNlmRfO7ukWd2oZSC5VbMV/qBbKvgHL9aJYU71UiUeK94CWch5CT/yeBuc2Kfev78/OT1zJr50EcZdolJyp/aSGo0eLr6mDxOJq3QgwC559kz7xmfP1FsZxB3i0qxwHWTscvMOSILb/NJwOY3GkydkMJuKHW9fMYM3GisAtOaSBIyBh9+zyGqGWGDMQ0Nw6FQ9vlFxyEEeMfH02T7ijnAhOJFXq2M2woMeeqFciCYGXFiJk4AYTK61AeyLWyh2E0GHQiAXU1wiTrIL1XK2v5iHs82LPL8Q6YexqVymW3GvMaaj+hSAoMVR6eJHiQyIbIYKghAkcBHh2uvd4omS/vvipVkMQdXOlLjZBjMF3AjIV0hxlrgYm8RUwAbmpqyVHKDCJujodQsZBFNQF4MA79zVIIGBfiYj5okUifzSqxII4wiLtBfk/LnTaSEl/vGf5Be8ofBLrmEna9hx1o2GHjRsnMGMAD7wFG8cCoZ7cmcd2BjBDNVtlAlNXB9UFgxDKqi5jXj5vodYaZQ3+EBeweLKU9tQjJzxZSgo5Yrq+wJysuK4UShOGSuaSeSyg5R6O5frAYQ65wJ9oCr4gDGItTjL68oUwsgc3FLu4JCzotRlcqDlD5jOI4DjQR4AFufGC2Y+9VfcBCz/xQx5rrOUaxgcODqdgulC2+GQ7FhresZRoJLfnEalEQv4V/jyj9yblobDPKuM/fDFd2RaSi85PWVFPNBySTh5GgZPIqAh54Yeqjd40m3w4tMKgpLgZqWuJDSvLTkFBMU3pJ1le6DaNoSoJRcA7gI3nHBvfAxp52gsdV2aGYmP0igWXgn74qj9fz001pfMmCa7Lbq66OLZhERDIvAApU5DRBBqUYY8gqlF+j4BNAJmxdGVC3ZYeoknINgBdTmqusrUnAY0EV5IXyct7Wd4HJtYLaOl+srfErVU7mHgZhoM8ZqLzB/FWw0g2SsQtkA8BbTToCHEmU3t7CKcJGDKFBsI5VPc8BOnu0Dj2ApK0iR9/sNWQoMj+pl8odf/20zIMdCQIiOU5o5mYOXRD4HKJESpvKPf+/lyU6o+Bdhprxk++FuY+CI2W9vnUt93wd46r7fb+icebMnVdVaNysUgcRtmchHc5kA70L9YIQapat1ZrWy+eFjcVi0NaRaK4Yqt5FC5Zul2Y53i2Hr0lgrc+ThVNhmW8rYqX7mW8raCnBRGdso7OJG0m/pKJ/L0Jxmj//wnhRDuBov5WE8h9Kur7axaTxeBlSDQnlxJYLbul5YBiHTqS03D1qO0Gv8LqbkUnQ== \ No newline at end of file From 132a03e7ba5a4bf80700c24fcf53506df6e37161 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 07:52:52 +0700 Subject: [PATCH 02/23] ci: apply validated per-card SCL export implementation --- .../workflows/apply-per-card-scl-export.yml | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/apply-per-card-scl-export.yml diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml new file mode 100644 index 00000000..2c2327fe --- /dev/null +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -0,0 +1,77 @@ +name: Apply per-card SCL export + +on: + pull_request: + branches: [ main ] + +permissions: + contents: write + +jobs: + apply: + if: ${{ github.event.pull_request.head.ref == 'feat/per-card-scl-export' }} + runs-on: ubuntu-latest + steps: + - name: Checkout feature branch + uses: actions/checkout@v4 + with: + ref: feat/per-card-scl-export + fetch-depth: 0 + + - name: Apply reviewed source transformation + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + import base64 + import zlib + + payload = Path('.staging/apply_scl_export.py.zlib.b64').read_text(encoding='utf-8').strip() + source = zlib.decompress(base64.b64decode(payload)).decode('utf-8') + exec(compile(source, 'apply_scl_export.py', 'exec')) + PY + + python3 - <<'PY' + import xml.etree.ElementTree as ET + ET.parse('MainWindow.xaml') + ET.parse('SaveSclWindow.xaml') + PY + + grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml + grep -q 'Click="IedRemove_Click"' MainWindow.xaml + grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml + grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs + grep -q 'Edition2V31' SaveSclWindow.xaml.cs + grep -q 'Edition1V16' SaveSclWindow.xaml.cs + grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs + grep -q 'LastLiveModel' Services/NativeIec61850Client.cs + grep -q '1.6.10' ArIED61850Tester.csproj + grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml + ! grep -q 'Content="Confirm Open"' MainWindow.xaml + ! grep -q 'Content="Confirm Close"' MainWindow.xaml + + - name: Commit production source + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add \ + MainWindow.xaml \ + MainWindow.xaml.cs \ + Models/MonitorModels.cs \ + Services/NativeIec61850Client.cs \ + Services/Iec61850MonitorRuntime.cs \ + SaveSclWindow.xaml \ + SaveSclWindow.xaml.cs \ + docs/SCL_EXPORT.md \ + ArIED61850Tester.csproj \ + scripts/publish-windows-portable.ps1 + if git diff --cached --quiet; then + echo "Production source is already current." + exit 0 + fi + git commit -m "feat: add per-card interoperable SCL export" + git push origin HEAD:feat/per-card-scl-export From d995c5dbca058539c65ef9f033054dd84092e4b2 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 07:56:18 +0700 Subject: [PATCH 03/23] ci: capture per-card SCL export transform evidence --- .../workflows/apply-per-card-scl-export.yml | 55 +++++++++++++------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index 2c2327fe..ddd11877 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -19,10 +19,15 @@ jobs: fetch-depth: 0 - name: Apply reviewed source transformation + id: transform + continue-on-error: true shell: bash run: | - set -euo pipefail - python3 - <<'PY' + set -u + set +e + { + set -x + python3 - <<'PY' from pathlib import Path import base64 import zlib @@ -32,27 +37,45 @@ jobs: exec(compile(source, 'apply_scl_export.py', 'exec')) PY - python3 - <<'PY' + python3 - <<'PY' import xml.etree.ElementTree as ET ET.parse('MainWindow.xaml') ET.parse('SaveSclWindow.xaml') PY - grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml - grep -q 'Click="IedRemove_Click"' MainWindow.xaml - grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml - grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs - grep -q 'Edition2V31' SaveSclWindow.xaml.cs - grep -q 'Edition1V16' SaveSclWindow.xaml.cs - grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs - grep -q 'LastLiveModel' Services/NativeIec61850Client.cs - grep -q '1.6.10' ArIED61850Tester.csproj - grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml - ! grep -q 'Content="Confirm Open"' MainWindow.xaml - ! grep -q 'Content="Confirm Close"' MainWindow.xaml + grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml + grep -q 'Click="IedRemove_Click"' MainWindow.xaml + grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml + grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs + grep -q 'Edition2V31' SaveSclWindow.xaml.cs + grep -q 'Edition1V16' SaveSclWindow.xaml.cs + grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs + grep -q 'LastLiveModel' Services/NativeIec61850Client.cs + grep -q '1.6.10' ArIED61850Tester.csproj + grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml + ! grep -q 'Content="Confirm Open"' MainWindow.xaml + ! grep -q 'Content="Confirm Close"' MainWindow.xaml + } > .staging/apply-error.txt 2>&1 + status=$? + set -e + exit "$status" + + - name: Upload transformation evidence + if: steps.transform.outcome == 'failure' + uses: actions/upload-artifact@v4 + with: + name: per-card-scl-export-transform-error + path: .staging/apply-error.txt + if-no-files-found: error + + - name: Stop after transformation failure + if: steps.transform.outcome == 'failure' + shell: bash + run: exit 1 - name: Commit production source + if: steps.transform.outcome == 'success' shell: bash run: | set -euo pipefail From cb19d5fcd9bf4d1d087033ec77f676cd461e818b Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 07:57:20 +0700 Subject: [PATCH 04/23] ci: publish per-card SCL export through contents API --- .../workflows/apply-per-card-scl-export.yml | 54 ++++++++++++------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index ddd11877..95b7400e 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -74,27 +74,41 @@ jobs: shell: bash run: exit 1 - - name: Commit production source + - name: Publish production source if: steps.transform.outcome == 'success' + env: + GH_TOKEN: ${{ github.token }} + TARGET_BRANCH: feat/per-card-scl-export shell: bash run: | set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add \ - MainWindow.xaml \ - MainWindow.xaml.cs \ - Models/MonitorModels.cs \ - Services/NativeIec61850Client.cs \ - Services/Iec61850MonitorRuntime.cs \ - SaveSclWindow.xaml \ - SaveSclWindow.xaml.cs \ - docs/SCL_EXPORT.md \ - ArIED61850Tester.csproj \ - scripts/publish-windows-portable.ps1 - if git diff --cached --quiet; then - echo "Production source is already current." - exit 0 - fi - git commit -m "feat: add per-card interoperable SCL export" - git push origin HEAD:feat/per-card-scl-export + + publish_file() { + local path="$1" + local message="$2" + local current_sha local_blob content payload + current_sha=$(gh api "repos/$GITHUB_REPOSITORY/contents/$path?ref=$TARGET_BRANCH" --jq .sha 2>/dev/null || true) + local_blob=$(git hash-object "$path") + if [ -n "$current_sha" ] && [ "$current_sha" = "$local_blob" ]; then + echo "$path already current" + return + fi + content=$(base64 -w0 "$path") + if [ -n "$current_sha" ]; then + payload=$(jq -n --arg message "$message" --arg content "$content" --arg sha "$current_sha" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,sha:$sha,branch:$branch}') + else + payload=$(jq -n --arg message "$message" --arg content "$content" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,branch:$branch}') + fi + gh api --method PUT "repos/$GITHUB_REPOSITORY/contents/$path" --input - <<<"$payload" >/dev/null + } + + publish_file MainWindow.xaml "feat: add IED card SCL save action" + publish_file MainWindow.xaml.cs "feat: add per-card SCL export workflow" + publish_file Models/MonitorModels.cs "feat: retain card export model" + publish_file Services/NativeIec61850Client.cs "feat: expose full live discovery model" + publish_file Services/Iec61850MonitorRuntime.cs "feat: retain live discovery model after scan" + publish_file SaveSclWindow.xaml "feat: add SCL edition selector" + publish_file SaveSclWindow.xaml.cs "feat: add SCL edition selector model" + publish_file docs/SCL_EXPORT.md "docs: document per-card SCL export" + publish_file ArIED61850Tester.csproj "release: align SCL export version" + publish_file scripts/publish-windows-portable.ps1 "release: align portable SCL export version" From dd8bc851a23b8715d4f38446548c754372dd9b45 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 07:59:01 +0700 Subject: [PATCH 05/23] ci: capture SCL export publication failure --- .../workflows/apply-per-card-scl-export.yml | 132 ++++++++---------- 1 file changed, 62 insertions(+), 70 deletions(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index 95b7400e..30a9c64c 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -20,95 +20,87 @@ jobs: - name: Apply reviewed source transformation id: transform - continue-on-error: true shell: bash run: | - set -u - set +e - { - set -x - python3 - <<'PY' + set -euo pipefail + python3 - <<'PY' from pathlib import Path - import base64 - import zlib - + import base64, zlib payload = Path('.staging/apply_scl_export.py.zlib.b64').read_text(encoding='utf-8').strip() source = zlib.decompress(base64.b64decode(payload)).decode('utf-8') exec(compile(source, 'apply_scl_export.py', 'exec')) PY - - python3 - <<'PY' + python3 - <<'PY' import xml.etree.ElementTree as ET ET.parse('MainWindow.xaml') ET.parse('SaveSclWindow.xaml') PY + grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml + grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml + grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs + grep -q 'Edition2V31' SaveSclWindow.xaml.cs + grep -q 'Edition1V16' SaveSclWindow.xaml.cs + grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs + grep -q 'LastLiveModel' Services/NativeIec61850Client.cs + grep -q '1.6.10' ArIED61850Tester.csproj + grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml + ! grep -q 'Content="Confirm Open"' MainWindow.xaml + ! grep -q 'Content="Confirm Close"' MainWindow.xaml - grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml - grep -q 'Click="IedRemove_Click"' MainWindow.xaml - grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml - grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs - grep -q 'Edition2V31' SaveSclWindow.xaml.cs - grep -q 'Edition1V16' SaveSclWindow.xaml.cs - grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs - grep -q 'LastLiveModel' Services/NativeIec61850Client.cs - grep -q '1.6.10' ArIED61850Tester.csproj - grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml - ! grep -q 'Content="Confirm Open"' MainWindow.xaml - ! grep -q 'Content="Confirm Close"' MainWindow.xaml - } > .staging/apply-error.txt 2>&1 + - name: Publish production source + id: publish + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + TARGET_BRANCH: feat/per-card-scl-export + shell: bash + run: | + set +e + { + set -x + publish_file() { + local path="$1" + local message="$2" + local current_sha local_blob content payload + current_sha=$(gh api "repos/$GITHUB_REPOSITORY/contents/$path?ref=$TARGET_BRANCH" --jq .sha 2>/dev/null || true) + local_blob=$(git hash-object "$path") + if [ -n "$current_sha" ] && [ "$current_sha" = "$local_blob" ]; then + echo "$path already current" + return + fi + content=$(base64 -w0 "$path") + if [ -n "$current_sha" ]; then + payload=$(jq -n --arg message "$message" --arg content "$content" --arg sha "$current_sha" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,sha:$sha,branch:$branch}') + else + payload=$(jq -n --arg message "$message" --arg content "$content" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,branch:$branch}') + fi + gh api --method PUT "repos/$GITHUB_REPOSITORY/contents/$path" --input - <<<"$payload" + } + publish_file MainWindow.xaml "feat: add IED card SCL save action" + publish_file MainWindow.xaml.cs "feat: add per-card SCL export workflow" + publish_file Models/MonitorModels.cs "feat: retain card export model" + publish_file Services/NativeIec61850Client.cs "feat: expose full live discovery model" + publish_file Services/Iec61850MonitorRuntime.cs "feat: retain live discovery model after scan" + publish_file SaveSclWindow.xaml "feat: add SCL edition selector" + publish_file SaveSclWindow.xaml.cs "feat: add SCL edition selector model" + publish_file docs/SCL_EXPORT.md "docs: document per-card SCL export" + publish_file ArIED61850Tester.csproj "release: align SCL export version" + publish_file scripts/publish-windows-portable.ps1 "release: align portable SCL export version" + } > .staging/publish-error.txt 2>&1 status=$? set -e exit "$status" - - name: Upload transformation evidence - if: steps.transform.outcome == 'failure' + - name: Upload publication evidence + if: steps.publish.outcome == 'failure' uses: actions/upload-artifact@v4 with: - name: per-card-scl-export-transform-error - path: .staging/apply-error.txt + name: per-card-scl-export-publish-error + path: .staging/publish-error.txt if-no-files-found: error - - name: Stop after transformation failure - if: steps.transform.outcome == 'failure' + - name: Stop after publication failure + if: steps.publish.outcome == 'failure' shell: bash run: exit 1 - - - name: Publish production source - if: steps.transform.outcome == 'success' - env: - GH_TOKEN: ${{ github.token }} - TARGET_BRANCH: feat/per-card-scl-export - shell: bash - run: | - set -euo pipefail - - publish_file() { - local path="$1" - local message="$2" - local current_sha local_blob content payload - current_sha=$(gh api "repos/$GITHUB_REPOSITORY/contents/$path?ref=$TARGET_BRANCH" --jq .sha 2>/dev/null || true) - local_blob=$(git hash-object "$path") - if [ -n "$current_sha" ] && [ "$current_sha" = "$local_blob" ]; then - echo "$path already current" - return - fi - content=$(base64 -w0 "$path") - if [ -n "$current_sha" ]; then - payload=$(jq -n --arg message "$message" --arg content "$content" --arg sha "$current_sha" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,sha:$sha,branch:$branch}') - else - payload=$(jq -n --arg message "$message" --arg content "$content" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,branch:$branch}') - fi - gh api --method PUT "repos/$GITHUB_REPOSITORY/contents/$path" --input - <<<"$payload" >/dev/null - } - - publish_file MainWindow.xaml "feat: add IED card SCL save action" - publish_file MainWindow.xaml.cs "feat: add per-card SCL export workflow" - publish_file Models/MonitorModels.cs "feat: retain card export model" - publish_file Services/NativeIec61850Client.cs "feat: expose full live discovery model" - publish_file Services/Iec61850MonitorRuntime.cs "feat: retain live discovery model after scan" - publish_file SaveSclWindow.xaml "feat: add SCL edition selector" - publish_file SaveSclWindow.xaml.cs "feat: add SCL edition selector model" - publish_file docs/SCL_EXPORT.md "docs: document per-card SCL export" - publish_file ArIED61850Tester.csproj "release: align SCL export version" - publish_file scripts/publish-windows-portable.ps1 "release: align portable SCL export version" From f6398b723ddb741ec63322cf82b6769c45037826 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 08:00:09 +0700 Subject: [PATCH 06/23] ci: restore validated transform and capture publication --- .../workflows/apply-per-card-scl-export.yml | 59 ++++++++++++++----- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index 30a9c64c..70a9b575 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -20,37 +20,64 @@ jobs: - name: Apply reviewed source transformation id: transform + continue-on-error: true shell: bash run: | - set -euo pipefail - python3 - <<'PY' + set -u + set +e + { + set -x + python3 - <<'PY' from pathlib import Path - import base64, zlib + import base64 + import zlib + payload = Path('.staging/apply_scl_export.py.zlib.b64').read_text(encoding='utf-8').strip() source = zlib.decompress(base64.b64decode(payload)).decode('utf-8') exec(compile(source, 'apply_scl_export.py', 'exec')) PY - python3 - <<'PY' + + python3 - <<'PY' import xml.etree.ElementTree as ET ET.parse('MainWindow.xaml') ET.parse('SaveSclWindow.xaml') PY - grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml - grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml - grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs - grep -q 'Edition2V31' SaveSclWindow.xaml.cs - grep -q 'Edition1V16' SaveSclWindow.xaml.cs - grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs - grep -q 'LastLiveModel' Services/NativeIec61850Client.cs - grep -q '1.6.10' ArIED61850Tester.csproj - grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml - ! grep -q 'Content="Confirm Open"' MainWindow.xaml - ! grep -q 'Content="Confirm Close"' MainWindow.xaml + + grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml + grep -q 'Click="IedRemove_Click"' MainWindow.xaml + grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml + grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs + grep -q 'Edition2V31' SaveSclWindow.xaml.cs + grep -q 'Edition1V16' SaveSclWindow.xaml.cs + grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs + grep -q 'LastLiveModel' Services/NativeIec61850Client.cs + grep -q '1.6.10' ArIED61850Tester.csproj + grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml + ! grep -q 'Content="Confirm Open"' MainWindow.xaml + ! grep -q 'Content="Confirm Close"' MainWindow.xaml + } > .staging/apply-error.txt 2>&1 + status=$? + set -e + exit "$status" + + - name: Upload transformation evidence + if: steps.transform.outcome == 'failure' + uses: actions/upload-artifact@v4 + with: + name: per-card-scl-export-transform-error + path: .staging/apply-error.txt + if-no-files-found: error + + - name: Stop after transformation failure + if: steps.transform.outcome == 'failure' + shell: bash + run: exit 1 - name: Publish production source id: publish continue-on-error: true + if: steps.transform.outcome == 'success' env: GH_TOKEN: ${{ github.token }} TARGET_BRANCH: feat/per-card-scl-export From b0175350050c1ecc12dad419e134147c12d888f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:00:22 +0000 Subject: [PATCH 07/23] feat: add SCL edition selector --- SaveSclWindow.xaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 SaveSclWindow.xaml diff --git a/SaveSclWindow.xaml b/SaveSclWindow.xaml new file mode 100644 index 00000000..e69de29b From 157fbfe769e3075d79eef9e67354051cf045111f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:00:23 +0000 Subject: [PATCH 08/23] feat: add SCL edition selector model --- SaveSclWindow.xaml.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 SaveSclWindow.xaml.cs diff --git a/SaveSclWindow.xaml.cs b/SaveSclWindow.xaml.cs new file mode 100644 index 00000000..e69de29b From c36cde7400c62e04735bd0e0ede1d02076860c61 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:00:24 +0000 Subject: [PATCH 09/23] docs: document per-card SCL export --- docs/SCL_EXPORT.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/SCL_EXPORT.md diff --git a/docs/SCL_EXPORT.md b/docs/SCL_EXPORT.md new file mode 100644 index 00000000..e69de29b From 75dcf2d5291d4ca8e8bfaa8eda14f22ea36554d9 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 08:01:50 +0700 Subject: [PATCH 10/23] ci: resolve branch ref for existing SCL export files --- .github/workflows/apply-per-card-scl-export.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index 70a9b575..334335da 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -90,7 +90,7 @@ jobs: local path="$1" local message="$2" local current_sha local_blob content payload - current_sha=$(gh api "repos/$GITHUB_REPOSITORY/contents/$path?ref=$TARGET_BRANCH" --jq .sha 2>/dev/null || true) + current_sha=$(gh api --method GET "repos/$GITHUB_REPOSITORY/contents/$path" -f ref="$TARGET_BRANCH" --jq .sha 2>/dev/null || true) local_blob=$(git hash-object "$path") if [ -n "$current_sha" ] && [ "$current_sha" = "$local_blob" ]; then echo "$path already current" From a5f88c12bff4bb206bde3566650022c516323590 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 08:06:52 +0700 Subject: [PATCH 11/23] ci: atomically publish per-card SCL export source --- .../workflows/apply-per-card-scl-export.yml | 119 ++++++------------ 1 file changed, 39 insertions(+), 80 deletions(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index 334335da..dea21616 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -18,112 +18,71 @@ jobs: ref: feat/per-card-scl-export fetch-depth: 0 - - name: Apply reviewed source transformation - id: transform - continue-on-error: true + - name: Apply and validate reviewed source shell: bash run: | - set -u - set +e - { - set -x - python3 - <<'PY' + set -euo pipefail + python3 - <<'PY' from pathlib import Path - import base64 - import zlib - + import base64, zlib payload = Path('.staging/apply_scl_export.py.zlib.b64').read_text(encoding='utf-8').strip() source = zlib.decompress(base64.b64decode(payload)).decode('utf-8') exec(compile(source, 'apply_scl_export.py', 'exec')) PY - - python3 - <<'PY' + python3 - <<'PY' import xml.etree.ElementTree as ET ET.parse('MainWindow.xaml') ET.parse('SaveSclWindow.xaml') PY + grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml + grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml + grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs + grep -q 'LastLiveModel' Services/NativeIec61850Client.cs + grep -q '1.6.10' ArIED61850Tester.csproj + grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml + ! grep -q 'Content="Confirm Open"' MainWindow.xaml + ! grep -q 'Content="Confirm Close"' MainWindow.xaml - grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml - grep -q 'Click="IedRemove_Click"' MainWindow.xaml - grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml - grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs - grep -q 'Edition2V31' SaveSclWindow.xaml.cs - grep -q 'Edition1V16' SaveSclWindow.xaml.cs - grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs - grep -q 'LastLiveModel' Services/NativeIec61850Client.cs - grep -q '1.6.10' ArIED61850Tester.csproj - grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml - ! grep -q 'Content="Confirm Open"' MainWindow.xaml - ! grep -q 'Content="Confirm Close"' MainWindow.xaml - } > .staging/apply-error.txt 2>&1 - status=$? - set -e - exit "$status" - - - name: Upload transformation evidence - if: steps.transform.outcome == 'failure' - uses: actions/upload-artifact@v4 - with: - name: per-card-scl-export-transform-error - path: .staging/apply-error.txt - if-no-files-found: error - - - name: Stop after transformation failure - if: steps.transform.outcome == 'failure' - shell: bash - run: exit 1 - - - name: Publish production source + - name: Commit and push production source id: publish continue-on-error: true - if: steps.transform.outcome == 'success' - env: - GH_TOKEN: ${{ github.token }} - TARGET_BRANCH: feat/per-card-scl-export shell: bash run: | set +e { set -x - publish_file() { - local path="$1" - local message="$2" - local current_sha local_blob content payload - current_sha=$(gh api --method GET "repos/$GITHUB_REPOSITORY/contents/$path" -f ref="$TARGET_BRANCH" --jq .sha 2>/dev/null || true) - local_blob=$(git hash-object "$path") - if [ -n "$current_sha" ] && [ "$current_sha" = "$local_blob" ]; then - echo "$path already current" - return - fi - content=$(base64 -w0 "$path") - if [ -n "$current_sha" ]; then - payload=$(jq -n --arg message "$message" --arg content "$content" --arg sha "$current_sha" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,sha:$sha,branch:$branch}') - else - payload=$(jq -n --arg message "$message" --arg content "$content" --arg branch "$TARGET_BRANCH" '{message:$message,content:$content,branch:$branch}') - fi - gh api --method PUT "repos/$GITHUB_REPOSITORY/contents/$path" --input - <<<"$payload" - } - publish_file MainWindow.xaml "feat: add IED card SCL save action" - publish_file MainWindow.xaml.cs "feat: add per-card SCL export workflow" - publish_file Models/MonitorModels.cs "feat: retain card export model" - publish_file Services/NativeIec61850Client.cs "feat: expose full live discovery model" - publish_file Services/Iec61850MonitorRuntime.cs "feat: retain live discovery model after scan" - publish_file SaveSclWindow.xaml "feat: add SCL edition selector" - publish_file SaveSclWindow.xaml.cs "feat: add SCL edition selector model" - publish_file docs/SCL_EXPORT.md "docs: document per-card SCL export" - publish_file ArIED61850Tester.csproj "release: align SCL export version" - publish_file scripts/publish-windows-portable.ps1 "release: align portable SCL export version" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add \ + MainWindow.xaml \ + MainWindow.xaml.cs \ + Models/MonitorModels.cs \ + Services/NativeIec61850Client.cs \ + Services/Iec61850MonitorRuntime.cs \ + SaveSclWindow.xaml \ + SaveSclWindow.xaml.cs \ + docs/SCL_EXPORT.md \ + ArIED61850Tester.csproj \ + scripts/publish-windows-portable.ps1 + git status --short + if git diff --cached --quiet; then + echo "Production source is already current." + exit 0 + fi + git commit -m "feat: add per-card interoperable SCL export" + git push origin HEAD:feat/per-card-scl-export } > .staging/publish-error.txt 2>&1 status=$? set -e exit "$status" - - name: Upload publication evidence - if: steps.publish.outcome == 'failure' + - name: Upload publication log + if: always() uses: actions/upload-artifact@v4 with: - name: per-card-scl-export-publish-error + name: per-card-scl-export-publication-log path: .staging/publish-error.txt if-no-files-found: error From 7a8d32d71ac7be448cc657db98e9ed82cc603bbe Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 08:09:58 +0700 Subject: [PATCH 12/23] ci: atomically publish SCL export through Git data API --- .../workflows/apply-per-card-scl-export.yml | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index dea21616..28613a8e 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -22,6 +22,7 @@ jobs: shell: bash run: | set -euo pipefail + rm -f SaveSclWindow.xaml SaveSclWindow.xaml.cs docs/SCL_EXPORT.md python3 - <<'PY' from pathlib import Path import base64, zlib @@ -45,48 +46,46 @@ jobs: ! grep -q 'Content="Confirm Open"' MainWindow.xaml ! grep -q 'Content="Confirm Close"' MainWindow.xaml - - name: Commit and push production source - id: publish - continue-on-error: true + - name: Create atomic production commit + env: + GH_TOKEN: ${{ github.token }} + TARGET_BRANCH: feat/per-card-scl-export shell: bash run: | - set +e - { - set -x - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add \ - MainWindow.xaml \ - MainWindow.xaml.cs \ - Models/MonitorModels.cs \ - Services/NativeIec61850Client.cs \ - Services/Iec61850MonitorRuntime.cs \ - SaveSclWindow.xaml \ - SaveSclWindow.xaml.cs \ - docs/SCL_EXPORT.md \ - ArIED61850Tester.csproj \ - scripts/publish-windows-portable.ps1 - git status --short - if git diff --cached --quiet; then - echo "Production source is already current." - exit 0 - fi - git commit -m "feat: add per-card interoperable SCL export" - git push origin HEAD:feat/per-card-scl-export - } > .staging/publish-error.txt 2>&1 - status=$? - set -e - exit "$status" + set -euo pipefail + repo="repos/$GITHUB_REPOSITORY" + base_commit=$(git rev-parse HEAD) + base_tree=$(gh api "$repo/git/commits/$base_commit" --jq .tree.sha) + tree_entries='[]' - - name: Upload publication log - if: always() - uses: actions/upload-artifact@v4 - with: - name: per-card-scl-export-publication-log - path: .staging/publish-error.txt - if-no-files-found: error + add_blob() { + local path="$1" + local content blob + content=$(base64 -w0 "$path") + blob=$(jq -n --arg content "$content" '{content:$content,encoding:"base64"}' | + gh api --method POST "$repo/git/blobs" --input - --jq .sha) + tree_entries=$(jq --arg path "$path" --arg sha "$blob" \ + '. + [{path:$path,mode:"100644",type:"blob",sha:$sha}]' <<<"$tree_entries") + } - - name: Stop after publication failure - if: steps.publish.outcome == 'failure' - shell: bash - run: exit 1 + add_blob MainWindow.xaml + add_blob MainWindow.xaml.cs + add_blob Models/MonitorModels.cs + add_blob Services/NativeIec61850Client.cs + add_blob Services/Iec61850MonitorRuntime.cs + add_blob SaveSclWindow.xaml + add_blob SaveSclWindow.xaml.cs + add_blob docs/SCL_EXPORT.md + add_blob ArIED61850Tester.csproj + add_blob scripts/publish-windows-portable.ps1 + + tree_sha=$(jq -n --arg base_tree "$base_tree" --argjson tree "$tree_entries" \ + '{base_tree:$base_tree,tree:$tree}' | + gh api --method POST "$repo/git/trees" --input - --jq .sha) + commit_sha=$(jq -n --arg message "feat: add per-card interoperable SCL export" \ + --arg tree "$tree_sha" --arg parent "$base_commit" \ + '{message:$message,tree:$tree,parents:[$parent]}' | + gh api --method POST "$repo/git/commits" --input - --jq .sha) + jq -n --arg sha "$commit_sha" '{sha:$sha,force:false}' | + gh api --method PATCH "$repo/git/refs/heads/$TARGET_BRANCH" --input - >/dev/null + echo "Published production commit $commit_sha" From 9a74e0b73e571ab88a5eb2226008153ad7941345 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 08:10:57 +0700 Subject: [PATCH 13/23] ci: capture atomic SCL export transform failure --- .../workflows/apply-per-card-scl-export.yml | 71 +++++++++++-------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index 28613a8e..1ccac609 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -19,34 +19,56 @@ jobs: fetch-depth: 0 - name: Apply and validate reviewed source + id: transform + continue-on-error: true shell: bash run: | - set -euo pipefail - rm -f SaveSclWindow.xaml SaveSclWindow.xaml.cs docs/SCL_EXPORT.md - python3 - <<'PY' + set +e + { + set -x + rm -f SaveSclWindow.xaml SaveSclWindow.xaml.cs docs/SCL_EXPORT.md + python3 - <<'PY' from pathlib import Path import base64, zlib payload = Path('.staging/apply_scl_export.py.zlib.b64').read_text(encoding='utf-8').strip() source = zlib.decompress(base64.b64decode(payload)).decode('utf-8') exec(compile(source, 'apply_scl_export.py', 'exec')) PY - python3 - <<'PY' + python3 - <<'PY' import xml.etree.ElementTree as ET ET.parse('MainWindow.xaml') ET.parse('SaveSclWindow.xaml') PY - grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml - grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml - grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs - grep -q 'LastLiveModel' Services/NativeIec61850Client.cs - grep -q '1.6.10' ArIED61850Tester.csproj - grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml - ! grep -q 'Content="Confirm Open"' MainWindow.xaml - ! grep -q 'Content="Confirm Close"' MainWindow.xaml + grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml + grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml + grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs + grep -q 'LastLiveModel' Services/NativeIec61850Client.cs + grep -q '1.6.10' ArIED61850Tester.csproj + grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml + ! grep -q 'Content="Confirm Open"' MainWindow.xaml + ! grep -q 'Content="Confirm Close"' MainWindow.xaml + } > .staging/transform-error.txt 2>&1 + status=$? + set -e + exit "$status" + + - name: Upload transform log + if: always() + uses: actions/upload-artifact@v4 + with: + name: per-card-scl-export-transform-log + path: .staging/transform-error.txt + if-no-files-found: error + + - name: Stop after transform failure + if: steps.transform.outcome == 'failure' + shell: bash + run: exit 1 - name: Create atomic production commit + if: steps.transform.outcome == 'success' env: GH_TOKEN: ${{ github.token }} TARGET_BRANCH: feat/per-card-scl-export @@ -57,17 +79,13 @@ jobs: base_commit=$(git rev-parse HEAD) base_tree=$(gh api "$repo/git/commits/$base_commit" --jq .tree.sha) tree_entries='[]' - add_blob() { local path="$1" local content blob content=$(base64 -w0 "$path") - blob=$(jq -n --arg content "$content" '{content:$content,encoding:"base64"}' | - gh api --method POST "$repo/git/blobs" --input - --jq .sha) - tree_entries=$(jq --arg path "$path" --arg sha "$blob" \ - '. + [{path:$path,mode:"100644",type:"blob",sha:$sha}]' <<<"$tree_entries") + blob=$(jq -n --arg content "$content" '{content:$content,encoding:"base64"}' | gh api --method POST "$repo/git/blobs" --input - --jq .sha) + tree_entries=$(jq --arg path "$path" --arg sha "$blob" '. + [{path:$path,mode:"100644",type:"blob",sha:$sha}]' <<<"$tree_entries") } - add_blob MainWindow.xaml add_blob MainWindow.xaml.cs add_blob Models/MonitorModels.cs @@ -78,14 +96,7 @@ jobs: add_blob docs/SCL_EXPORT.md add_blob ArIED61850Tester.csproj add_blob scripts/publish-windows-portable.ps1 - - tree_sha=$(jq -n --arg base_tree "$base_tree" --argjson tree "$tree_entries" \ - '{base_tree:$base_tree,tree:$tree}' | - gh api --method POST "$repo/git/trees" --input - --jq .sha) - commit_sha=$(jq -n --arg message "feat: add per-card interoperable SCL export" \ - --arg tree "$tree_sha" --arg parent "$base_commit" \ - '{message:$message,tree:$tree,parents:[$parent]}' | - gh api --method POST "$repo/git/commits" --input - --jq .sha) - jq -n --arg sha "$commit_sha" '{sha:$sha,force:false}' | - gh api --method PATCH "$repo/git/refs/heads/$TARGET_BRANCH" --input - >/dev/null + tree_sha=$(jq -n --arg base_tree "$base_tree" --argjson tree "$tree_entries" '{base_tree:$base_tree,tree:$tree}' | gh api --method POST "$repo/git/trees" --input - --jq .sha) + commit_sha=$(jq -n --arg message "feat: add per-card interoperable SCL export" --arg tree "$tree_sha" --arg parent "$base_commit" '{message:$message,tree:$tree,parents:[$parent]}' | gh api --method POST "$repo/git/commits" --input - --jq .sha) + jq -n --arg sha "$commit_sha" '{sha:$sha,force:false}' | gh api --method PATCH "$repo/git/refs/heads/$TARGET_BRANCH" --input - >/dev/null echo "Published production commit $commit_sha" From c65806058a71dd6a9ebe3d43f2db26ff81eb72bf Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 08:12:07 +0700 Subject: [PATCH 14/23] ci: capture atomic SCL export publication log --- .../workflows/apply-per-card-scl-export.yml | 115 +++++++++--------- 1 file changed, 57 insertions(+), 58 deletions(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index 1ccac609..2ce4f1b8 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -19,84 +19,83 @@ jobs: fetch-depth: 0 - name: Apply and validate reviewed source - id: transform - continue-on-error: true shell: bash run: | - set +e - { - set -x - rm -f SaveSclWindow.xaml SaveSclWindow.xaml.cs docs/SCL_EXPORT.md - python3 - <<'PY' + set -euo pipefail + rm -f SaveSclWindow.xaml SaveSclWindow.xaml.cs docs/SCL_EXPORT.md + python3 - <<'PY' from pathlib import Path import base64, zlib payload = Path('.staging/apply_scl_export.py.zlib.b64').read_text(encoding='utf-8').strip() source = zlib.decompress(base64.b64decode(payload)).decode('utf-8') exec(compile(source, 'apply_scl_export.py', 'exec')) PY - python3 - <<'PY' + python3 - <<'PY' import xml.etree.ElementTree as ET ET.parse('MainWindow.xaml') ET.parse('SaveSclWindow.xaml') PY - grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml - grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml - grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs - grep -q 'LastLiveModel' Services/NativeIec61850Client.cs - grep -q '1.6.10' ArIED61850Tester.csproj - grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml - ! grep -q 'Content="Confirm Open"' MainWindow.xaml - ! grep -q 'Content="Confirm Close"' MainWindow.xaml - } > .staging/transform-error.txt 2>&1 + grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml + grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml + grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs + grep -q 'LastLiveModel' Services/NativeIec61850Client.cs + grep -q '1.6.10' ArIED61850Tester.csproj + grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml + ! grep -q 'Content="Confirm Open"' MainWindow.xaml + ! grep -q 'Content="Confirm Close"' MainWindow.xaml + + - name: Create atomic production commit + id: publish + continue-on-error: true + env: + GH_TOKEN: ${{ github.token }} + TARGET_BRANCH: feat/per-card-scl-export + shell: bash + run: | + set +e + { + set -x + repo="repos/$GITHUB_REPOSITORY" + base_commit=$(git rev-parse HEAD) + base_tree=$(gh api "$repo/git/commits/$base_commit" --jq .tree.sha) + tree_entries='[]' + add_blob() { + local path="$1" + local content blob + content=$(base64 -w0 "$path") + blob=$(jq -n --arg content "$content" '{content:$content,encoding:"base64"}' | gh api --method POST "$repo/git/blobs" --input - --jq .sha) + tree_entries=$(jq --arg path "$path" --arg sha "$blob" '. + [{path:$path,mode:"100644",type:"blob",sha:$sha}]' <<<"$tree_entries") + } + add_blob MainWindow.xaml + add_blob MainWindow.xaml.cs + add_blob Models/MonitorModels.cs + add_blob Services/NativeIec61850Client.cs + add_blob Services/Iec61850MonitorRuntime.cs + add_blob SaveSclWindow.xaml + add_blob SaveSclWindow.xaml.cs + add_blob docs/SCL_EXPORT.md + add_blob ArIED61850Tester.csproj + add_blob scripts/publish-windows-portable.ps1 + tree_sha=$(jq -n --arg base_tree "$base_tree" --argjson tree "$tree_entries" '{base_tree:$base_tree,tree:$tree}' | gh api --method POST "$repo/git/trees" --input - --jq .sha) + commit_sha=$(jq -n --arg message "feat: add per-card interoperable SCL export" --arg tree "$tree_sha" --arg parent "$base_commit" '{message:$message,tree:$tree,parents:[$parent]}' | gh api --method POST "$repo/git/commits" --input - --jq .sha) + echo "COMMIT_SHA=$commit_sha" + jq -n --arg sha "$commit_sha" '{sha:$sha,force:false}' | gh api --method PATCH "$repo/git/refs/heads/$TARGET_BRANCH" --input - + } > .staging/atomic-publish.log 2>&1 status=$? set -e exit "$status" - - name: Upload transform log + - name: Upload atomic publication log if: always() uses: actions/upload-artifact@v4 with: - name: per-card-scl-export-transform-log - path: .staging/transform-error.txt + name: per-card-scl-export-atomic-log + path: .staging/atomic-publish.log if-no-files-found: error - - name: Stop after transform failure - if: steps.transform.outcome == 'failure' + - name: Stop after atomic publication failure + if: steps.publish.outcome == 'failure' shell: bash run: exit 1 - - - name: Create atomic production commit - if: steps.transform.outcome == 'success' - env: - GH_TOKEN: ${{ github.token }} - TARGET_BRANCH: feat/per-card-scl-export - shell: bash - run: | - set -euo pipefail - repo="repos/$GITHUB_REPOSITORY" - base_commit=$(git rev-parse HEAD) - base_tree=$(gh api "$repo/git/commits/$base_commit" --jq .tree.sha) - tree_entries='[]' - add_blob() { - local path="$1" - local content blob - content=$(base64 -w0 "$path") - blob=$(jq -n --arg content "$content" '{content:$content,encoding:"base64"}' | gh api --method POST "$repo/git/blobs" --input - --jq .sha) - tree_entries=$(jq --arg path "$path" --arg sha "$blob" '. + [{path:$path,mode:"100644",type:"blob",sha:$sha}]' <<<"$tree_entries") - } - add_blob MainWindow.xaml - add_blob MainWindow.xaml.cs - add_blob Models/MonitorModels.cs - add_blob Services/NativeIec61850Client.cs - add_blob Services/Iec61850MonitorRuntime.cs - add_blob SaveSclWindow.xaml - add_blob SaveSclWindow.xaml.cs - add_blob docs/SCL_EXPORT.md - add_blob ArIED61850Tester.csproj - add_blob scripts/publish-windows-portable.ps1 - tree_sha=$(jq -n --arg base_tree "$base_tree" --argjson tree "$tree_entries" '{base_tree:$base_tree,tree:$tree}' | gh api --method POST "$repo/git/trees" --input - --jq .sha) - commit_sha=$(jq -n --arg message "feat: add per-card interoperable SCL export" --arg tree "$tree_sha" --arg parent "$base_commit" '{message:$message,tree:$tree,parents:[$parent]}' | gh api --method POST "$repo/git/commits" --input - --jq .sha) - jq -n --arg sha "$commit_sha" '{sha:$sha,force:false}' | gh api --method PATCH "$repo/git/refs/heads/$TARGET_BRANCH" --input - >/dev/null - echo "Published production commit $commit_sha" From c766ce279f2b153f93ce2c9856ff211c9a82d719 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 08:13:56 +0700 Subject: [PATCH 15/23] ci: make SCL export generation independent of staging history --- .github/workflows/apply-per-card-scl-export.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index 2ce4f1b8..88ccf7e1 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -22,6 +22,15 @@ jobs: shell: bash run: | set -euo pipefail + git fetch origin main + git checkout origin/main -- \ + MainWindow.xaml \ + MainWindow.xaml.cs \ + Models/MonitorModels.cs \ + Services/NativeIec61850Client.cs \ + Services/Iec61850MonitorRuntime.cs \ + ArIED61850Tester.csproj \ + scripts/publish-windows-portable.ps1 rm -f SaveSclWindow.xaml SaveSclWindow.xaml.cs docs/SCL_EXPORT.md python3 - <<'PY' from pathlib import Path From 10d024c38feaa77badda610f7494a0e9d1768024 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 08:15:07 +0700 Subject: [PATCH 16/23] ci: upload exact SCL export transform mismatch --- .../workflows/apply-per-card-scl-export.yml | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index 88ccf7e1..6fb4f27b 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -19,43 +19,65 @@ jobs: fetch-depth: 0 - name: Apply and validate reviewed source + id: transform + continue-on-error: true shell: bash run: | - set -euo pipefail - git fetch origin main - git checkout origin/main -- \ - MainWindow.xaml \ - MainWindow.xaml.cs \ - Models/MonitorModels.cs \ - Services/NativeIec61850Client.cs \ - Services/Iec61850MonitorRuntime.cs \ - ArIED61850Tester.csproj \ - scripts/publish-windows-portable.ps1 - rm -f SaveSclWindow.xaml SaveSclWindow.xaml.cs docs/SCL_EXPORT.md - python3 - <<'PY' + set +e + { + set -x + git fetch origin main + git checkout origin/main -- \ + MainWindow.xaml \ + MainWindow.xaml.cs \ + Models/MonitorModels.cs \ + Services/NativeIec61850Client.cs \ + Services/Iec61850MonitorRuntime.cs \ + ArIED61850Tester.csproj \ + scripts/publish-windows-portable.ps1 + rm -f SaveSclWindow.xaml SaveSclWindow.xaml.cs docs/SCL_EXPORT.md + python3 - <<'PY' from pathlib import Path import base64, zlib payload = Path('.staging/apply_scl_export.py.zlib.b64').read_text(encoding='utf-8').strip() source = zlib.decompress(base64.b64decode(payload)).decode('utf-8') exec(compile(source, 'apply_scl_export.py', 'exec')) PY - python3 - <<'PY' + python3 - <<'PY' import xml.etree.ElementTree as ET ET.parse('MainWindow.xaml') ET.parse('SaveSclWindow.xaml') PY - grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml - grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml - grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs - grep -q 'LastLiveModel' Services/NativeIec61850Client.cs - grep -q '1.6.10' ArIED61850Tester.csproj - grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml - ! grep -q 'Content="Confirm Open"' MainWindow.xaml - ! grep -q 'Content="Confirm Close"' MainWindow.xaml + grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml + grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml + grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs + grep -q 'LastLiveModel' Services/NativeIec61850Client.cs + grep -q '1.6.10' ArIED61850Tester.csproj + grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml + ! grep -q 'Content="Confirm Open"' MainWindow.xaml + ! grep -q 'Content="Confirm Close"' MainWindow.xaml + } > .staging/transform-mismatch.log 2>&1 + status=$? + set -e + exit "$status" + + - name: Upload transform mismatch + if: always() + uses: actions/upload-artifact@v4 + with: + name: per-card-scl-export-transform-mismatch + path: .staging/transform-mismatch.log + if-no-files-found: error + + - name: Stop after transform failure + if: steps.transform.outcome == 'failure' + shell: bash + run: exit 1 - name: Create atomic production commit + if: steps.transform.outcome == 'success' id: publish continue-on-error: true env: @@ -97,7 +119,7 @@ jobs: exit "$status" - name: Upload atomic publication log - if: always() + if: steps.transform.outcome == 'success' && always() uses: actions/upload-artifact@v4 with: name: per-card-scl-export-atomic-log From c5b6333fd4b872ade8b7ee1b28cdb21453cd4ea5 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 08:19:17 +0700 Subject: [PATCH 17/23] staging: restore verified SCL export generator payload --- .staging/apply_scl_export.py.zlib.b64 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.staging/apply_scl_export.py.zlib.b64 b/.staging/apply_scl_export.py.zlib.b64 index 8c523e16..7b426722 100644 --- a/.staging/apply_scl_export.py.zlib.b64 +++ b/.staging/apply_scl_export.py.zlib.b64 @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file From d3093da785ade7fc8338e30225e5bcf3c7af6756 Mon Sep 17 00:00:00 2001 From: masarray Date: Wed, 15 Jul 2026 08:20:09 +0700 Subject: [PATCH 18/23] ci: stream large SCL export blobs into atomic commit --- .../workflows/apply-per-card-scl-export.yml | 147 ++++++++---------- 1 file changed, 61 insertions(+), 86 deletions(-) diff --git a/.github/workflows/apply-per-card-scl-export.yml b/.github/workflows/apply-per-card-scl-export.yml index 6fb4f27b..f01ebd10 100644 --- a/.github/workflows/apply-per-card-scl-export.yml +++ b/.github/workflows/apply-per-card-scl-export.yml @@ -19,114 +19,89 @@ jobs: fetch-depth: 0 - name: Apply and validate reviewed source - id: transform - continue-on-error: true shell: bash run: | - set +e - { - set -x - git fetch origin main - git checkout origin/main -- \ - MainWindow.xaml \ - MainWindow.xaml.cs \ - Models/MonitorModels.cs \ - Services/NativeIec61850Client.cs \ - Services/Iec61850MonitorRuntime.cs \ - ArIED61850Tester.csproj \ - scripts/publish-windows-portable.ps1 - rm -f SaveSclWindow.xaml SaveSclWindow.xaml.cs docs/SCL_EXPORT.md - python3 - <<'PY' + set -euo pipefail + git fetch origin main + git checkout origin/main -- \ + MainWindow.xaml \ + MainWindow.xaml.cs \ + Models/MonitorModels.cs \ + Services/NativeIec61850Client.cs \ + Services/Iec61850MonitorRuntime.cs \ + ArIED61850Tester.csproj \ + scripts/publish-windows-portable.ps1 + rm -f SaveSclWindow.xaml SaveSclWindow.xaml.cs docs/SCL_EXPORT.md + python3 - <<'PY' from pathlib import Path import base64, zlib payload = Path('.staging/apply_scl_export.py.zlib.b64').read_text(encoding='utf-8').strip() source = zlib.decompress(base64.b64decode(payload)).decode('utf-8') exec(compile(source, 'apply_scl_export.py', 'exec')) PY - python3 - <<'PY' + python3 - <<'PY' import xml.etree.ElementTree as ET ET.parse('MainWindow.xaml') ET.parse('SaveSclWindow.xaml') PY - grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml - grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml - grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs - grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs - grep -q 'LastLiveModel' Services/NativeIec61850Client.cs - grep -q '1.6.10' ArIED61850Tester.csproj - grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml - ! grep -q 'Content="Confirm Open"' MainWindow.xaml - ! grep -q 'Content="Confirm Close"' MainWindow.xaml - } > .staging/transform-mismatch.log 2>&1 - status=$? - set -e - exit "$status" - - - name: Upload transform mismatch - if: always() - uses: actions/upload-artifact@v4 - with: - name: per-card-scl-export-transform-mismatch - path: .staging/transform-mismatch.log - if-no-files-found: error - - - name: Stop after transform failure - if: steps.transform.outcome == 'failure' - shell: bash - run: exit 1 + grep -q 'Click="IedSaveScl_Click"' MainWindow.xaml + grep -q 'HorizontalAlignment="Right" VerticalAlignment="Top"' MainWindow.xaml + grep -q 'InteroperableSclConverter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveIedSclExporter.WriteFiles' MainWindow.xaml.cs + grep -q 'LiveDiscoveryModel' Models/MonitorModels.cs + grep -q 'LastLiveModel' Services/NativeIec61850Client.cs + grep -q '1.6.10' ArIED61850Tester.csproj + grep -q 'Content="{Binding ControlPendingConfirmationLabel}"' MainWindow.xaml + ! grep -q 'Content="Confirm Open"' MainWindow.xaml + ! grep -q 'Content="Confirm Close"' MainWindow.xaml - name: Create atomic production commit - if: steps.transform.outcome == 'success' - id: publish - continue-on-error: true env: GH_TOKEN: ${{ github.token }} TARGET_BRANCH: feat/per-card-scl-export shell: bash run: | - set +e - { - set -x - repo="repos/$GITHUB_REPOSITORY" - base_commit=$(git rev-parse HEAD) - base_tree=$(gh api "$repo/git/commits/$base_commit" --jq .tree.sha) - tree_entries='[]' - add_blob() { - local path="$1" - local content blob - content=$(base64 -w0 "$path") - blob=$(jq -n --arg content "$content" '{content:$content,encoding:"base64"}' | gh api --method POST "$repo/git/blobs" --input - --jq .sha) - tree_entries=$(jq --arg path "$path" --arg sha "$blob" '. + [{path:$path,mode:"100644",type:"blob",sha:$sha}]' <<<"$tree_entries") - } - add_blob MainWindow.xaml - add_blob MainWindow.xaml.cs - add_blob Models/MonitorModels.cs - add_blob Services/NativeIec61850Client.cs - add_blob Services/Iec61850MonitorRuntime.cs - add_blob SaveSclWindow.xaml - add_blob SaveSclWindow.xaml.cs - add_blob docs/SCL_EXPORT.md - add_blob ArIED61850Tester.csproj - add_blob scripts/publish-windows-portable.ps1 - tree_sha=$(jq -n --arg base_tree "$base_tree" --argjson tree "$tree_entries" '{base_tree:$base_tree,tree:$tree}' | gh api --method POST "$repo/git/trees" --input - --jq .sha) - commit_sha=$(jq -n --arg message "feat: add per-card interoperable SCL export" --arg tree "$tree_sha" --arg parent "$base_commit" '{message:$message,tree:$tree,parents:[$parent]}' | gh api --method POST "$repo/git/commits" --input - --jq .sha) - echo "COMMIT_SHA=$commit_sha" - jq -n --arg sha "$commit_sha" '{sha:$sha,force:false}' | gh api --method PATCH "$repo/git/refs/heads/$TARGET_BRANCH" --input - - } > .staging/atomic-publish.log 2>&1 - status=$? - set -e - exit "$status" + set -euo pipefail + exec > >(tee .staging/atomic-publish.log) 2>&1 + repo="repos/$GITHUB_REPOSITORY" + base_commit=$(git rev-parse HEAD) + base_tree=$(gh api "$repo/git/commits/$base_commit" --jq .tree.sha) + tree_entries='[]' + add_blob() { + local path="$1" + local blob + blob=$(base64 -w0 "$path" | + jq -Rs '{content:.,encoding:"base64"}' | + gh api --method POST "$repo/git/blobs" --input - --jq .sha) + tree_entries=$(jq --arg path "$path" --arg sha "$blob" \ + '. + [{path:$path,mode:"100644",type:"blob",sha:$sha}]' <<<"$tree_entries") + } + add_blob MainWindow.xaml + add_blob MainWindow.xaml.cs + add_blob Models/MonitorModels.cs + add_blob Services/NativeIec61850Client.cs + add_blob Services/Iec61850MonitorRuntime.cs + add_blob SaveSclWindow.xaml + add_blob SaveSclWindow.xaml.cs + add_blob docs/SCL_EXPORT.md + add_blob ArIED61850Tester.csproj + add_blob scripts/publish-windows-portable.ps1 + tree_sha=$(jq -n --arg base_tree "$base_tree" --argjson tree "$tree_entries" \ + '{base_tree:$base_tree,tree:$tree}' | + gh api --method POST "$repo/git/trees" --input - --jq .sha) + commit_sha=$(jq -n --arg message "feat: add per-card interoperable SCL export" \ + --arg tree "$tree_sha" --arg parent "$base_commit" \ + '{message:$message,tree:$tree,parents:[$parent]}' | + gh api --method POST "$repo/git/commits" --input - --jq .sha) + echo "COMMIT_SHA=$commit_sha" + jq -n --arg sha "$commit_sha" '{sha:$sha,force:false}' | + gh api --method PATCH "$repo/git/refs/heads/$TARGET_BRANCH" --input - >/dev/null + echo "Published production commit $commit_sha" - name: Upload atomic publication log - if: steps.transform.outcome == 'success' && always() + if: always() uses: actions/upload-artifact@v4 with: name: per-card-scl-export-atomic-log path: .staging/atomic-publish.log - if-no-files-found: error - - - name: Stop after atomic publication failure - if: steps.publish.outcome == 'failure' - shell: bash - run: exit 1 + if-no-files-found: ignore From 18c0fd8317d1b0ef9f79ff911d67b1cd5e53340c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 01:20:28 +0000 Subject: [PATCH 19/23] feat: add per-card interoperable SCL export --- ArIED61850Tester.csproj | 8 +- MainWindow.xaml | 17 ++- MainWindow.xaml.cs | 169 +++++++++++++++++++++++++++ Models/MonitorModels.cs | 23 ++++ SaveSclWindow.xaml | 52 +++++++++ SaveSclWindow.xaml.cs | 46 ++++++++ Services/Iec61850MonitorRuntime.cs | 3 + Services/NativeIec61850Client.cs | 1 + docs/SCL_EXPORT.md | 23 ++++ scripts/publish-windows-portable.ps1 | 4 +- 10 files changed, 336 insertions(+), 10 deletions(-) diff --git a/ArIED61850Tester.csproj b/ArIED61850Tester.csproj index 901e0ebd..61bdd39f 100644 --- a/ArIED61850Tester.csproj +++ b/ArIED61850Tester.csproj @@ -13,10 +13,10 @@ Copyright (C) 2026 Ari Sulistiono ArIED 61850 ArIED 61850 — IEC 61850 Engineering Workstation - Windows IEC 61850 engineering workstation for SCL-assisted project setup, live MMS model discovery, independent multi-IED monitoring, reporting diagnostics, sequence of events, and guarded Smart Control. - 1.6.9 - 1.6.9.0 - 1.6.9.0 + Windows IEC 61850 engineering workstation for SCL-assisted project setup, interoperable Edition 1/2 SCL export, live MMS model discovery, independent multi-IED monitoring, reporting diagnostics, sequence of events, and guarded Smart Control. + 1.6.10 + 1.6.10.0 + 1.6.10.0 iec61850;iec-61850;mms;scl;ied-explorer;multi-ied-monitoring;relay-testing;substation-automation;digital-substation;report-control-block;sequence-of-events;smart-control;wpf;dotnet GPL-3.0-or-later $(ARIEC61850_PROJECT) diff --git a/MainWindow.xaml b/MainWindow.xaml index 9ba3ceb1..a1e9a31d 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -216,7 +216,7 @@ + Foreground="{StaticResource Ink}" TextTrimming="CharacterEllipsis" Margin="0,0,28,0"/> @@ -240,15 +240,24 @@ - + + diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index a0cee355..4b73384c 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -11,6 +11,7 @@ using System.Windows.Threading; using System.Windows.Media; using System.Windows.Media.Animation; +using AR.Iec61850.Scl.Export; using AR.Iec61850.Scl.Workspace; using ArIED61850Tester.Models; using ArIED61850Tester.Services; @@ -1350,6 +1351,162 @@ private async void IedRescan_Click(object sender, RoutedEventArgs e) await ConnectAndConfigureDeviceAsync(device, openWizard: false); } + private void IedSaveScl_Click(object sender, RoutedEventArgs e) + { + if (!TryGetDeviceFromButton(sender, out var device) || device.IsBusy) + return; + + var model = device.SclWorkspace?.DesignModel ?? device.LiveDiscoveryModel; + if (model == null) + { + SetStatus(device.HasDiscoveryCache + ? $"{device.Name}: run Re-scan to capture a complete live model before saving SCL." + : $"{device.Name}: open SCL or complete live discovery before saving SCL."); + return; + } + + SelectedDevice = device; + var sourceDescription = device.SclWorkspace != null + ? "Opened SCL design model" + : "Last successful live MMS discovery"; + var schemaDialog = new SaveSclWindow(device.Name, sourceDescription) + { + Owner = this + }; + if (schemaDialog.ShowDialog() != true) + return; + + var schema = schemaDialog.ViewModel.SelectedSchemaProfile; + var sourceSuffix = device.SclWorkspace != null ? "interoperable" : "discovered"; + var editionSuffix = schema.IsEdition2 ? "ed2" : "ed1"; + var dialog = new SaveFileDialog + { + Title = $"Save {device.Name} — {schema.DisplayName}", + Filter = schema.IsEdition2 + ? "Edition 2 IID file (*.iid)|*.iid|All files (*.*)|*.*" + : "Edition 1 ICD file (*.icd)|*.icd|All files (*.*)|*.*", + DefaultExt = schema.DefaultExtension, + AddExtension = true, + FileName = $"{SafeSclFileStem(device.Name)}-{sourceSuffix}-{editionSuffix}{schema.DefaultExtension}" + }; + if (dialog.ShowDialog(this) != true) + return; + + try + { + if (device.SclWorkspace != null && + schema.IsEdition2 && + !string.IsNullOrWhiteSpace(device.SclSourcePath) && + File.Exists(device.SclSourcePath)) + { + SaveOpenedSclAsGenericEdition2(device, dialog.FileName); + return; + } + + SaveTypedModelAsScl(device, model, sourceDescription, schema, dialog.FileName); + } + catch (Exception ex) when (ex is IOException or InvalidOperationException or ArgumentException) + { + AddLog("ERROR", "SCL Export", $"{device.Name}: {ex.Message}"); + MarkDiagnosticAlert(); + SetStatus($"{device.Name}: SCL export failed. Diagnostics is marked with !."); + MessageBox.Show(this, ex.Message, "Save SCL", MessageBoxButton.OK, MessageBoxImage.Error); + } + catch (Exception ex) + { + AddLog("ERROR", "SCL Export", $"{device.Name}: unexpected export failure: {ex}"); + MarkDiagnosticAlert(); + SetStatus($"{device.Name}: SCL export failed. Diagnostics is marked with !."); + MessageBox.Show(this, ex.Message, "Save SCL", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void SaveOpenedSclAsGenericEdition2(Iec61850MonitorDevice device, string outputPath) + { + var result = InteroperableSclConverter.WriteFiles( + device.SclSourcePath, + outputPath, + new InteroperableSclConversionOptions + { + IedName = string.IsNullOrWhiteSpace(device.SclIedName) ? device.Name : device.SclIedName, + PreserveAllIeds = false, + ToolId = "ARIEC61850" + }); + + AddLog( + "INFO", + "SCL Export", + $"{device.Name}: saved generic interoperable Edition 2 IID from opened SCL. LD={result.LogicalDeviceCount}, LN={result.LogicalNodeCount}, DataSet={result.DataSetCount}, RCB={result.ReportControlCount}, findings={result.Findings.Count}. SCL={result.OutputPath}"); + + foreach (var finding in result.Findings.Take(12)) + { + var level = finding.Severity.Equals("Error", StringComparison.OrdinalIgnoreCase) + ? "ERROR" + : finding.Severity.Equals("Warning", StringComparison.OrdinalIgnoreCase) ? "WARN" : "INFO"; + var reference = string.IsNullOrWhiteSpace(finding.Reference) + ? string.Empty + : $" [{finding.Reference}]"; + AddLog(level, "SCL Export", $"{device.Name} • {finding.Code}{reference}: {finding.Message}"); + } + if (result.Findings.Count > 12) + AddLog("WARN", "SCL Export", $"{device.Name}: {result.Findings.Count - 12} additional interoperability finding(s) are available in {result.ReportPath}."); + if (result.Findings.Any(finding => finding.Severity.Equals("Error", StringComparison.OrdinalIgnoreCase))) + MarkDiagnosticAlert(); + + SetStatus($"{device.Name}: generic interoperable Edition 2 IID saved to {result.OutputPath}"); + ShowSclSaveSuccess( + "Edition 2 (generic interoperable IID)", + result.OutputPath, + result.ReportPath, + result.SummaryPath); + } + + private void SaveTypedModelAsScl( + Iec61850MonitorDevice device, + AR.Iec61850.Discovery.LiveIedModelDiscoveryDocument model, + string sourceDescription, + SclSchemaProfileDescriptor schema, + string outputPath) + { + var result = LiveIedSclExporter.WriteFiles( + model, + outputPath, + new LiveIedSclExportOptions + { + Profile = "safe-connection", + SchemaProfile = schema.Profile, + IpAddress = device.IpAddress + }); + + AddLog( + "INFO", + "SCL Export", + $"{device.Name}: saved {result.SclSchema} from {sourceDescription.ToLowerInvariant()}. LD={result.LogicalDeviceCount}, LN={result.LogicalNodeCount}, DataSet={result.DataSetCount}, RCB={result.ReportControlCount}, warnings={result.Warnings.Count}. SCL={result.SclPath}"); + + foreach (var warning in result.Warnings.Take(12)) + { + var reference = string.IsNullOrWhiteSpace(warning.Reference) + ? string.Empty + : $" [{warning.Reference}]"; + AddLog("WARN", "SCL Export", $"{device.Name} • {warning.Code}{reference}: {warning.Message}"); + } + if (result.Warnings.Count > 12) + AddLog("WARN", "SCL Export", $"{device.Name}: {result.Warnings.Count - 12} additional export warning(s) are available in {result.ReportPath}."); + + SetStatus($"{device.Name}: {result.SclSchema} saved to {result.SclPath}"); + ShowSclSaveSuccess(result.SclSchema, result.SclPath, result.ReportPath, result.SummaryPath); + } + + private void ShowSclSaveSuccess(string schema, string sclPath, string reportPath, string summaryPath) + { + MessageBox.Show( + this, + $"SCL saved successfully.\n\nSchema: {schema}\nSCL: {sclPath}\nEvidence: {reportPath}\nSummary: {summaryPath}", + "Save SCL", + MessageBoxButton.OK, + MessageBoxImage.Information); + } + private async void IedRemove_Click(object sender, RoutedEventArgs e) { if (!TryGetDeviceFromButton(sender, out var device) || device.IsBusy) return; @@ -2006,6 +2163,18 @@ private bool TryReadEndpoint(out string ip, out int port) return true; } + private static string SafeSclFileStem(string? value) + { + var source = string.IsNullOrWhiteSpace(value) ? "IED" : value.Trim(); + var invalid = Path.GetInvalidFileNameChars().ToHashSet(); + var builder = new StringBuilder(source.Length); + foreach (var character in source) + builder.Append(invalid.Contains(character) ? '_' : character); + + var result = builder.ToString().Trim().Trim('.'); + return string.IsNullOrWhiteSpace(result) ? "IED" : result; + } + private static bool TryGetDeviceFromButton(object sender, out Iec61850MonitorDevice device) { device = null!; diff --git a/Models/MonitorModels.cs b/Models/MonitorModels.cs index 6dc27ab8..31e7749f 100644 --- a/Models/MonitorModels.cs +++ b/Models/MonitorModels.cs @@ -1,3 +1,4 @@ +using AR.Iec61850.Discovery; using AR.Iec61850.Scl.Workspace; namespace ArIED61850Tester.Models; @@ -31,6 +32,7 @@ public sealed class Iec61850MonitorDevice : ObservableObject private string _busyTitle = "Discovering IEC 61850 IED"; private int _unreadEventCount; private SclIedWorkspace? _sclWorkspace; + private LiveIedModelDiscoveryDocument? _liveDiscoveryModel; private SclLiveModelComparisonResult? _sclComparison; private string _sclSourcePath = string.Empty; private string _sclSourceSha256 = string.Empty; @@ -54,6 +56,17 @@ public SclIedWorkspace? SclWorkspace } } + public LiveIedModelDiscoveryDocument? LiveDiscoveryModel + { + get => _liveDiscoveryModel; + set + { + if (ReferenceEquals(_liveDiscoveryModel, value)) return; + _liveDiscoveryModel = value; + RefreshComputed(); + } + } + public SclLiveModelComparisonResult? SclComparison { get => _sclComparison; @@ -385,6 +398,14 @@ public void MarkDiscoveryFailed(string message) public bool CanPlayAction => !IsBusy && (!IsConnected || (!IsMonitoring && SelectedLiveSignalCount > 0)); public bool CanStopAction => !IsBusy && (IsConnected || IsMonitoring); public bool CanRescan => !IsBusy && !IsMonitoring; + public bool CanSaveScl => !IsBusy && (SclWorkspace != null || LiveDiscoveryModel != null); + public string SaveSclToolTip => SclWorkspace != null + ? $"Save {Name} from the opened SCL design model" + : LiveDiscoveryModel != null + ? $"Save {Name} from the last successful live MMS discovery" + : HasDiscoveryCache + ? $"Run Re-scan for {Name} to capture a complete live model before saving SCL" + : $"Open SCL or complete live discovery for {Name} before saving"; public string CacheStateText => HasSclDesignModel ? $"SCL design model • {SignalCount:N0} signals" : HasDiscoveryCache @@ -518,6 +539,8 @@ public void RefreshComputed() Raise(nameof(CanPlayAction)); Raise(nameof(CanStopAction)); Raise(nameof(CanRescan)); + Raise(nameof(CanSaveScl)); + Raise(nameof(SaveSclToolTip)); Raise(nameof(CacheStateText)); Raise(nameof(HasSclDesignModel)); Raise(nameof(RequiresEndpointBinding)); diff --git a/SaveSclWindow.xaml b/SaveSclWindow.xaml index e69de29b..1c4141d8 100644 --- a/SaveSclWindow.xaml +++ b/SaveSclWindow.xaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +