This commit is contained in:
GukSang.Jin
2026-04-02 17:32:09 +08:00
parent d1bf076c0e
commit 12275fad0f
27 changed files with 447 additions and 66 deletions

View File

@@ -0,0 +1 @@
False

View File

@@ -0,0 +1 @@
03ad10bc-67b0-4a31-808b-125fa8839ba8

View File

@@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACqxUy07kRhT9Fau2sT31fnjHdDMKQgxoaCVRdvW4hgrucqtchiDEv0d2wwwwyiJSNpZ1dR/nnnPqPqFk94A6dHI4nN5DKhOqUYlriGIqG8waSnZYdkJ0XLWSasUx+xPVKJ7DI+qQ4t4Tb6BhIKHhAYvGsd43IYAhmARMBV962psJdU/IxjbAffTQjtNvkKc4JtSh32MK48My28bWD+Mc2jwOcJamYpNfwGxPr893l1fNhhO8ufx8zJxgWhq0MaAOBWlwz7FpvOxNwz0VjWE9biyDIG3PiMH+WBZTgZzs0E7h7geGMJYExXe0pazFDTXouUbBFrvAdnaC3eNhQbLStF3i9Rpef7sndA8ZdbR+JfTY7pMf4qf95OY4hE/rF9XokMcD5BJhZWRdchhsiWOqXmZshgipoBp9XkquZ+9hmlCHvthhgtfwabqJCb5ku4eHMd99Pc5tv57uUI2u8vgX+HJlyy3qEFHeSu2lp72kRJBeEMcd9JLJnhJChPfG9cYISwFT7RRog4WTmHojnDGoRpfX1dVgSz/m/TvBro8anP0HCa5PNqfJugHCx5V2Nt9AQR0SUjhgvabSSRusYbonFpgJ3jEpQDsVNCjFKLNeeKyJJ4oIa5QQSjJKUI0urL+NCaqzLeoQZtRYxXuDPaaWCGGt7rWR3DLGwWjvRGCGaOuEocoCts5wqYXl2llJFarRZkwlpnmcp+osFbjJq2JvNri8rk6yv40FfJnzosUfcrH+VR7D7Ev1w2oEt7glmC1FcznMpfoGIWbwZaVkl+eXfh9KqKQYL1DmnCGValG3+tVOi8Tcc0cosTiYnhGqlfasx0IRwqhhwmntQu9xTzhRVDCClfTMOKm8wiTQxZewOLtaH5NyRIugRSMJFw23lDdWaN4IiqUwCph2/B3H1eUQ/h+e33j7zfp6YYy0Sq6svVL68ly4RjU6h5xgeEPZRfR5nMa+VC9urd6R+GbONk6HwT7+NO4XTzkTnsH3/M0t+LufzfttTsvRrNZX8BBT8/eq/Hb0d5CrxTk2puU+fK+4mIcSd7cZbIBwMQb42PS5Rnuw05xhv57l9VIkyCuM7Xx031m6iMMQJ/BjChPqqJFGtIpz/YL33xM1My3j/Pn5+fkfAAAA//8DAEvu+ZMFBgAA

View File

@@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACqyUTW/cNhCG/4rAsyTz+0O31E5Rwwi8iBdp0duQHMVEtNKCopwGxv73grID170V6I0YcIYvn3lnnskMJyQD+XA+f3zCuaykJSXtIU657qjoODtSPSg9UNM7R7k18k/SknSHP8hAjAyBBYedQI2djFR1XoyhixEdoyxSrmStCV9XMjwTSH3EpxSwX9YvmNe0zGQgv6c5Lt/r25D6MC1b7PMy4e28FphDFXPz8eHueH/oriWj1/e/vNxcca0F+hTJQIRmo0HNO+617aS0tPNC2I76EWXkJkSoQiD1aS6YZ5j6NX570xCXMmMJA++56GnHHbm0JEKBKtvDiscf56pkx3RT4+0e3o/DM3nCTAbe/gT6Uu4qTOlq3fyETzidIa+Yr8JyOsEcSUvOeTljLgl3NE+Y/bKmUqmGYMBaxqIFI5WJViguTBQUo6VGRRcdaKlHHpQao7fOyFFGzmTUKKU3pCU7vGmCkpa5edV+PSWcC2nJ/UNzmKCMSz69w//wQvT2PwCtsuttqoQ2BqUQApz2nGI0OmjNfQBqRs+RBaTKSeOYoUw4Tp1Q2lgTgbpAWvIJwmOasbm9IQOhgjswcnQ0UA5MKQA7WqclCCHR2eBVFI5Z8MpxA0jBO6mtAmk9aF4JXC9zSfO2bGtzOxf8mncWZCC/wrTiC4UPOTymgqFsuQL6Q9cvHfISt1CaN3Mw2tOeUVGTtnLeSvMZY8oYClZUx7y91vtXCtec0iplyxnn0hygPDa/wfpIBiKD9IwzoNGNgnFrbBAjVYYxwZ1Q3lofx0BHJpnhSjBqdBDOaxMMZZFXA2H1YrPbnwMPbrSyc8LJTiotOhet6Jw1oraR+pG9Y9zcTzXvf+D8E9eryaQlLbnDPOP0DxyfUsjLuoyleXVb8w7Q522ua6fZnfc9zd1feydulvANc1M7CWmuE/bavEtLTgjrlvG0b63hmRzqeDXHfXkJ03MrRUseCuSynd/CljJ1uVwufwMAAP//AwCxFX/O+gQAAA==

View File

@@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACqyUy27dNhCGX0XgWpJ5Ey/apXaKukZgIzbaIrshObKJ6FAHFOU0MM67F5RP4Lq7AtkRA87w5zf/zAtJcEAykg/H48dnTGUlLSlxD3HKVUdFx9kDVeOgRqp7y9UwMPuFtCTe4HcyEi29Z95iJ1BhJwMdOicm34WAllEWKB9krQmPKxlfCMQ+4HP02C/rH5jXuCQykj9jCsu3+jbE3s/LFvq8zHid1gLJVzFXH+9vHm7vukvJ6OXtL683V1xrgT4GMhKh2KRR8Y47ZTopDe2cEKajbkIZuPYBqhCIfUwFc4K5X8PXNw1hKQmLH3nPRU87bsmpJQEKVNkOVnz4fqxKdkxXNd7u4f04vpBnzGTk7Q+gr+Uu/BwvynKc8RnnI+QV84VfDgdIgbTkmJcj5hJxR7P/dp6hxCU158cu54ipkJbc3jd3M5RpyYd3vO5fEVz/DwLPmF29TQehtEYphACrHKcYtPJKceeB6slxZB7pYKW2TFMmLKdWDEobHYBaT1ryOC8O5t/XnZ/l1JlANVccJVoulbKeojA4DRppAKWcMt4GYTSXJhgBFoBZh9pb5auyT+CfYsLm+oqMhApuQcvJUk85sGEAMJOxSoIQEq3xbgjCMgNusFwDUnBWKjOANA4U16Qll0sqMW3LtjbXqeBj3tmSkfwK84qvVD9k/xQL+rLlCvwvVYXc5SVsvjRv7mC0pz2joiZt5biV5jOGmNEXrOgf8nau958UrjilVcqWM6bS3EF5an6D9YmMRHrpGGdAg50E40YbLyY6aMYEt2JwxrgweToxyTQfBKNaeWGd0l5TFnh1EFYzNrv/Q0AmGbWd1tp30nvdgZC8G+ggndc+KMXfMW5u55r3Ezj/wHU2rTSkJTeYE87/wvEp+rysy1Sas3ubd4A+b6nunWZ38reYur/3Tlwt/ivmpnYSYqojdm7eqSUHhHXLeNjX1vhC7up8NQ/79hK650aKltwXyGU7voUNZcPpdDr9AwAA//8DAKenY437BAAA

View File

@@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACqxUTU/kRhD9K1ZfY3v7292+sTOsghAL2hklUW79UYYOpj1qtyEIzX+P2sAusMohUi6WVeqqevXeq3pC0dwB6tHJ4XB6DzHPqEY5rCGKqWwwayjZY9kL2WPVasKoZt2fqEbhHB5RjzruHHEaGgYSGu6xaCwbXOM9aIKJx1TwUtNcz6h/Qia0Hu6Dg3aaf4M0hymiHv0eop8eSm8TWjdOi2/TNMJZnLOJroDZnu7O95dXzYYTvLn8/PxyhrkUaINHPWKSDB1I2lArVcO5wo1lTDXYDsA97Zw3BYgJbYgZUjRjO/vbHxj8lCNk19OWshY3VKNjjbzJpsC2Zob946EgWWnalni9htff/gndQ0I9rV8JfS73yY3h091slzD6T+sX1eiQpgOkHGBlZB1yHE0OU6xeemzGADGjGn0uKbvFOZhn1KMvZpzhNXwar0OEL8ncwcOUbr8+922/nu5Rja7S9Be4fGXyDeoR6ZyRyklHB0mJIIMgllsYJJMDJYQI57QdtBaGAqbKdqA0FlZi6rSwWqMaXe6qq9HkYUp37wTbPWtw9h8k2J1sTqOxI/iPI+1NuoaMegTMYsc51WpwxBGuzWAH7pTWcrCactoZ4AS45Npqxp3hWmhNbKcEtUoIVKML425ChOpsi3qEGdWm44PGDlNDhDBGDUpLbhjjoJWzwjNNlLFCl9rYWM2lEoYrayTtUI02U8whLtMyV2cxw3VaFXszweWuOknuJmRweUlFiz9kGfcqTX5xufphNYJb3BLMStKSD0uuvoEPCVxeKdmn5aXehxQqKcYFypISxFwVdatfzVwk5o5bQonBXg+MUNUpxwYsOrJurLBKWT84PBBOOioYwZ10TFvZuQ4TT4svoTi7WpdJGk2FZrgxjLGGE8YaTQbdUA3EM46lgfccV5ej/394fuPtN+OrwhhpO7my9krpy7pwhWp0DinC+Iayi+DSNE9Drl7cWr0j8U2fbZgPo3n8qd0vjnImHIPv7zc34G5/Nu+3JZajWa1b8BBi8/eq/HZyt5Cq4hwTYrkP3zMuljGH/U0C48FfTB4+Fj3W6A7MvCS4W8/yeikipBXGdnl231m8COMYZnBT9DPqldKt6IR+Qftvz7RSLRFYHo/H4z8AAAD//wMAel4/lAIGAAA=

View File

@@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACqxUTW/jNhD9KwLPkkJSlEQJ6CF1smiQpjYSb7fojR/DmIhMGhSVbBDkvxeUlbWzvfTQGzHkzLx57w3fkBN7QD26PByun8HFEeUo2jlEMW0KXBWUbHHT102PeVnXlDe4+RvlyN7CK+pRy5QiqoOiggYKpnFdyMqoQmvoCCYa05qlmuJxRP0bErbU8GwVlH78E8JovUM9+mad9i+pt7ClGvyky+AHuHFjFE4lMFfXD7fb9aZYMYJX61+PL0cYU4HSatSjqiGmhYYWVDa8YIzjQlYVL7A0wDRtlRYJiLCldRGCE0M56qcTBu2jg6h6WtKqxAXt0HuOtIgiwZZihO3rISGZabpK8XwOz8f+DT1DQD3NPwg9lrtQg73Yj3Kyg76IIjxCNEHs4cWHJ3gWA8rRIfgDhGhh5mf9kJ0gEVzikjYUY5SjO6F21kF2c5WthzQwrmgnWmY6rDAVpK6F4IZ3DRNVxaDjSta66ggXsu5oKwAL2bGG14JxKRraohzdQnAwnHW8syr40ZuYLZJknzCsvIvWTX4asxsX4TGIeMz7IoYRUI42kxzsuLsHoV+3/n5Kd24a0pyrKQRwMduIuMt+E+MO9YgpJgklAuvOVITylqvK4LolpKJdVUvOpTYKG8JIS+uK4LZRVSebVrWYaKpRjtZTPExx0eabddffz3Bsg93vQZ9AbGcJNoOIxof9jQYXrbFJuTMPzrYbhnm2bKm8Giy4+EmG/0eClXfGPk4/mMTGaFaZmvNGdpJgoKblDLdGYq1MpTpdS2hpi1sOvFMYy5oRLjuFNZCaJUo2wetJxZ+NRHCFcnQ/ubTf2U1i5cW64nvDToRtgo9e+eHE2L8UXfn9wY82JlbMIvvXEe68S7fL3/E5+dLHUwzS/mTzypKu0TXFvFCK6ILxShdd25qiq0FyAwzaWqIcXXn1BCFL5hPWzWJ9+G39kF0GtbMRVJxC6vzXPM5R5y8fq3ZiovzjervyAS4Ph3yJ/vLMy2Tu9UP2YYxPdvg6ptc7P6YhYpjO/PUAg/mAdeayoyeze9A2gIrz1faYuFD0yXlL1mWI1ggVx7Qhv3s1O2Kx3/Lk535n/P/IPjY/02Bxw1KJ8bnO/HHOHvhP/+b7+/v7PwAAAP//AwB7sYkoLAYAAA==

View File

@@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACqxTy27UMBT9leiu49SvOE52MC2iqlArWgFi59g3jNXUHjlOAVXz78jpVFB2SOysK9/j8/ITBPOAMMCbw+HiEUNeoIbstxGnXBEqCGd3VA2tGqhueiZaIfhXqMFf4U8YoJPWMtsjEaiQSEdbMorJEuewZ5Q5yltZMM23BYYnML5x+OgtNnH5hGnxMcAAn31w8Xt52/jGznF1TYozXoYlm2ALmfOL26u76xuyk4zurt8+31xwKQCNdzCAUGzqUHHCR6WJlJqSUQhN6DihdLyzzhQixjc+ZEzBzM3i7n9zcDEHzHbgDRcNJbyHYw3OZFNoj2bBu5+HwmSz6bzM6228HYcneMQEA69fDH2GO7OzP7Px4cEEdzb54Jc91HBI8YApe9ws2VTOs8k+hur0yG72GDLUcH1b3cwmTzE9vPLp9ln65T8o/2Ds3gesLs9hACp4bzo59dRSbljbGqMn3StphJDYazu2TvRMm7HteWeQmrGXSrdG6tEo3kENuxiyD2tcl+oyZPyWNgUwwDszL/jM/U2ye5/R5jUVWV9UIYI/fN5FVwYMarhJ0a02V7+jYLShDaOiYKz5sObqIzqf0GYseu/SeoL/a4UrTmlhtqaEIVc3Ju+r92bZwwDSypFxZqjrJ8G47rQVE207xgTvRTtqPbrJ0olJ1vFWMNopK/pRdbajzHFXeJfkq61sivVKdB0lVAlNpOWOGOUm4iztbOco14y/sry6nsvef7D9xa5TU6SGGq4wBZz/sOODtykuccrVqTLVK4M+rqF88mqrz3cfyI8tmPNo7zFVJVjjQ+nzKcvj8Xj8BQAA//8DAF1B08wrBAAA

View File

@@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACqxUS2/jNhD+K8KcJYUPvW+79i4apKmDtbstehuRI4eITBoUlW0Q+L8XlJ1mve2hh96E0eCb7zHDV7B4IOjgw/H46ZlsmCCFYJaSYKLKmMwE37GqK6uONTmTVVMy+QekYO7oBTqoC6W4aimTVFFWaFZmvRxUpjW1nHHNRFlETNxP0L0CmlzTs1GUu+kr+ck4Cx38Zqx23+JsNLka3axz70a6tVNAqyKZ9aft3W7zkK0Kzlabj+fOiaYIkBsNHciKDzVVIhN91WRF0bCsl7LJWD9QoUWtNEYiaHJjA3mLYz7pp3cO2gVLQXUiFzJnmWjhlILGgJF2jxPtXo6RyWLTOtbTpbx8dq/wTB46kb4Zeoa7UaO5OUz9bEZ9M7r93tj9ytnB7GePIQ5O4ejdkXwwtBi02SbvnDjLWS4qwRikcI/q0VhKbtfJZoyKmRQt1sXQMsUE8rJEbIamrQqUsqC2UX2pZcsb7MtW1EgM+7aomhKLpsdK1JDCHXlL43cT743ybnJDSC6ZJFccVs4GY2c3T8mtDbS/iOjgM44TxYbZe7IhecDwmPyE0yN0UKii54Ij0+0guWjqRsmBlTXnUrSy7Jum14NiAy94LUrJWV0p2fZVrWrGtdCQwo78wVgcf3b7ffT573nXP9Y04DyGrZv9sjWTfoIUPhqL/uXc8etE+tL1yzmoN6Rl2cZxEZRcsl6Nhmy48v7/8f3BOz2r8GPUnElI4cts4wkmtzHkb8Zmf1bFDzK+401xH5PlBGopK2SNyqhElRWMRIai5VlRtqWs6gaxrSCFtVNP5JOYJRp7BbbZJh+8ejSBVJh9tOD3ZfZmmzyMGAbnD1fnupnDcQ7JF9LGkwoUWez8HKE+m5H+QfZN98Xfojnv1OTeer+S791kQnxa7o01Bxwhhe350BdD/uOdX6G+s/rXfYEOcA4OTqfT6S8AAAD//wMASF6U/hIFAAA=

View File

@@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACqyTT2/cNhDFv4owZ1HmP1GkbunaRQ0jWCNepEFvFDmKCcvUgqKcBMZ+94LaNRL3FqAXQRhoZt77zdMrRPuM0MOH4/HmBWNeoIYcthKnXBEqCGcHqvpW9VQ3hlHO2u4fqCHc4Q/ooZPOMWeQCFRIpKctGcToiPdoGGWe8laWmfbrAv0r2NB4fAkOm3n5jGkJc4Qe/g7Rz9/KbhsaN82rb9I84W1cso2uiLm+ebg77O/JTjK62/9x/nLBpQxogocehGJjh4oTPihNpNSUDEJoQocRpeed87YIsaEJMWOKdmoW//RTg59zxOx63nDRUMINnGrwNtsie7ALHn4ci5IN03Wp11t5e+1f4QUT9Lx+A3oed+WmcPW8DGuY/NX5me3yVJwe03zElANuXDar02RzmGN12bSbAsYMNewfqvvJ5nFOz+9gPZz93/6G/UPZfvMd3ZrR7+Y15tIroIaP1j2GiNXtNfRABTe2k6OhjnLL2tZaPWqjpBVCotFuaL0wTNuhNbyzSO1gpNKtlXqwindQw26OOcR1XpfqNmb8mjZv0MOfdlrw7OpDco8ho8trKoa/qCLxPs1+dbn6eRpGG9owWlTu13xcc/UJfUjoMhbrh7Re5v2nhStOaZGypoQxV/c2P1Z/2eURepBODowzS70ZBeO6006MtO0YE9yIdtB68KOjI5Os461gtFNOmEF1rqPMcw81YElCtYXPeqWoc0hGrTyRQ+eJMV1HqOGojDZOSf6OcbWfSt//wPkN1yU0UkMNd5giTr/g+Bhcmpd5zNUlPdU7QJ/WWH76akvStxDJ9+0S17N7wlSVS9oQS74vxzudTqd/AQAA//8DAGz3os87BAAA

View File

@@ -1,5 +0,0 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACqSUTW/cOAyG/4rBs+3owx+yb+kkxQZBkaAZdBd7oyW6EeJIA0lOGwTz3xfyTJF2L4tFL4JAi69ePqT8Bg6fCUa4PByuX8ilCCUku4UEE13FZCX4nnVj241M1QNTrOuHv6EEe0uvMELfaM31QJWkjqrGsLaa5KwrY2jgjBsm2iZr4tcI4xugrQ29WE21j18oROsdjPCndcZ/y3ejrfXiV1MHv9CNiwmdzmaurh9u93f31a7hbHf34XQyUswCtTUwguz43FMnKjF1qmoaxapJSlWxaabGiF4bzEbQ1tYlCg6XOpqndw/GJ0dJj6IWsmaVGOBYgsGE2faEkfavh+xkw3SV4+UW3rbjG7xQgFGUP4Ce5C70Yi+e47TaxVyc1oTxKW7rjDr58AolHII/UEiWNkaXMdLztLzuMT59PJ3J23j9nfSayOz86lKuWEAJG6NlwWS9K84Wd4sll7aPKVgXrf4vKQ4l3D0U9wum2YfnX1rycKJ88z8gf0L9aB0VN1cwApNiwL6ZB6aZQN62iGpWQ9eglA0NSk+tkQNXOLWD6JEYTkPTqRYbNWEneihh512ybvVrLG5coq9hKxdG+IhLpJP3y6AfbSKd1pAZ/NVlI/fBm1Wn4r3NnNWs5kzmpDUd1lR8JmMD6US5wH1Yz3r/ShGdYCxbWUMgl4p7TI/FHxgfYYRGNxMXHJkZZsmF6pWWM2t7zqUYZDspNZlZs5k3vBet5KzvtBymrtc940YYKIHyVBXbIDOakfLbQ0Zt1bB2qLAlVUnF275F3Umd3b8zLu6WLe/3Of/AdZ6jRkEJtxQcLT/h+GR18NHPqTjPSPELoM+ryz+QYpuXb9ZV37dOXHn9RKHInUTr8ls5N+94PB7/AQAA//8DAGR77q+HBAAA

View File

@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUTW/cNhCG/4rAsyjz+0O31E5R1whsxEZb9DYkR7YQrbSgKKeBsf+9oHaDxL31Rgw4My8fvjNvZIYDkp58OB4/vuJcVtKSMu4hwYShTFHGnzjvNe+F7TjTTgr5N2nJeIffSE+sipFHj1SiQaoS0zTIIdKU0HPGExNa1ZrwvJL+jcDYJXwdI3bL+gfmdVxm0pM/xzktX2tvGLs4LVvq8jLh7bwWmGMVc/Px8e7p/oFeK86u738531xxrQW6MZGeyMRj1AGpUzZR5T2n3glLJTLGvBhSlPycNs4F8wxTt6YvPzSkpcxYYi86ITtGhSenliQoUGUHWPHp27Eq2THd1Hi7h/dj/0ZeMZNetN+BnstdxWm8KstxwlecjpBXzFdxORxgTqQlx7wcMZcRdzT7a6cJyrjMzaXZ9TTiXEhL7h+bhwnKsOTDO16PZwS3/4PAK+ZQbzMtjbWopJTgTRAMkzXRGBEiMDsEgTwi015Zzy3j0gvmpTbW2QTMR9KS52kJMP2+7vy8YMElZoURqNALZYyPDKXDQVtkCYwJxkWfpLNCueQkeADuA9roTawm+QTxZZyxub0hPWFSeLBq8CwyAVxrADc4bxRIqdC7GHSSnjsI2gsLyCB4ZZwG5QIYYUlLrpe5jPO2bGtzOxd8zjtb0pNfYVrxTPVDji9jwVi2XIH/ZaqQh7ykLZbmhzs461gnWOV3v5XjVprPmMaMsWBF/5S3S73/phjBWJWy5YxzaR6gvDS/wfpCeqKiClxwYMkPkgtnXZQD05ZzKbzUwbmQhsgGrrgVWnJmTZQ+GBst40lUB2E1Y3P2f+QuWY4UnBVUKQ/UhwAUAjgTxGCkFu8YN/dTzcPBJ69VpEY7R5XzgTolgYbAuNfGh8H5n5BcjKkcackd5hmnn578aYx5WZehNBeHNu8gfN7mulua3a1fx5n+s9O+WeIXzE39LRjnOkaXDzq15ICwbhkP+2rq38hDnaHmad9QynaCedOSxwK5bMdL2OvOSsVPp9PpXwAAAP//AwAvyYBn3wQAAA==

View File

@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUTW/jNhD9KwLPosIvSaRuu/YuGqSpg7W7LXqjyJFDRCYNiso2CPzfF5SdZt3toTdhNHrvzZs3ekVeHwB16MPx+OkZfJpQiZJbSoywBhOBCd1R2tW0Y21FJZFMir9QidwdvKAOtcIYahRgDg1gYUmNez4YbC0oSqglrBYZU+8n1L0i7SoLz85AFaavECcXPOrQH87b8C1za1eZMcy2imGEWz8l7U0Ws/60vdttHvBKULLafDx3TjBlgMpZ1CFuqTF1D1iK1mKhFMVKshZzIIQoNljD6fkz5xNEr8dqsk/vGmxIHpLpWMV4RTBT6FQiq5POsns9we7lmJUsNq1zvVzKy2P3ip4hoo6Vb4ae4W7M6G4OUz+70d6MYb93fr8KfnD7OeqUiUt0jOEIMTlYDNpsi3dNlFSkYg0jBJXoXptH56G4XRebMU9MuLaU9AY3bU+w0JxiSWSPKasHLSVXvZaoRHcQPYw/oN47E8MUhlRcfC+ueFbBJ+fnME/FrU+wvwjt0Gc9TpAb5hjBp+JBp8fiFz09og4JI3rKqCZWDZwy2UrDB1K3lHKmeN1L2dvBkIEK2rKaU9I2hqu+aU1LqGUWlWgH8eC8Hn8N+3328h++6xdrGPQ8pm2Y45KMyT6hEn10XseXc8fvE9hL12/nZbwhLYEax2Wg4rLP1ejApyt/F2+Z0q0YFDGEaVrXWstBqkZozgUoafrackWl7mvFWg1E90o0stZC9rphLSrRQwx2NumndZIcwy+zz2dW3OZFfnMe/92If43xg27ImSuWmGvatqSmCteaNFgYIFixWmBNh7rRDWkHnsnXwTxBLPIutfNXYJtt8SGaR5fApDlmC/5cuDfb4mHUaQjxcHWSmzkd51R8AesimARZxS7OGeqzG+EnsW9zX/wVOYOr4Kfw1vsVYh8ml/Lv4955d9AjKtH2fMyLIf/zlq9Q31X9Z16ydXMK6HQ6nb4DAAD//wMAOAifPPYEAAA=

View File

@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUS2/dNhCF/4rAtSQPH+JDu9ROUdcIbMQXbdHdkBzZQnSlC4pyGhj3vxeUb5C4u+6IA87w8OPhvLIZj8R69uF0+vhCc15ZzfK4SwKEbkA1wA+c9x3vlWqhM04b9zer2XhH31jPjAqBB0eNJE2NitA1Xg6hiZEcBx5BdKr0xKeV9a8MxzbSyxioXdY/KK3jMrOe/TnOcflazsaxDdOyxTYtE93Oa8Y5FDM3Hx/vDvcPzbXicH3/y9vOldbSoB1jMYJDJEW28VzxRqnBNmi1avSgjYqorefyrWycM6UZp3aNX354iEueKYdetEK20AjHzjWLmLHY9rjS4dupONkx3RS93uV92b+yF0qsF/V3oG/trsI0XuXlNNELTSdMK6WrsByPOEdWs1NaTpTySDua/bbThHlc5upy2PU00pxZze4fq4cJ87Ck4ztej28Ibv8HgRdKnvVMQie1MaSklOi0F0DR6KC18AHBDF4QDwSdU8ZxA1w6AU522lgTEVxgNXuaFo/T7+vOzwnwNoIRWpAiJ5TWLgBJS0NnCCJq7bUNLkprhLLRSnSI3HkywelQQvIJw/M4U3V7w3oGUjg0anAQQCDvOkQ7WKcVSqnI2eC7KB236DsnDBKgd0rbDpX1qIVhNbte5jzO27Kt1e2c6SntbFnPfsVppTeqH1J4HjOFvKUC/C9djDykJW4hVz/SwaGFVgAvRVs+bbn6THFMFDIV9Ie0Xfr9t0QLgGJlS4nmXD1gfq5+w/WZ9UwF5bngCNENkgtrbJADdIZzKZzsvLU+DgEGrrgRneRgdJDOaxMM8ChKgqiEsdrzryyA5o6aqKxo1IDQOOldYwYf0QJK6tw7xtX9VOpAYuTgQ6ONh0ah5I0F6xsuugGtlc6j/QnJJZiqaHeUZpp+uvKnMaRlXYZcXRJavYPweZvLbKn2tH4d5+afnfbNEr5Qqspr4TiXb3R5oHPNjoTrlui4j6b+lT2UP1Qd9gmlRGukEzV7zJjydrrIpmsddOJ8Pp//BQAA//8DAPnP2SDfBAAA

View File

@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACoxUwW7jNhD9FYFnUSEpSqJ027V30SBNHazdbdHbiBw5RGTSoKhsg8D/vqDsNOtuD70Jo6c3b9680StxcEDSkQ/H46dndHEiOYl2KQkmasokZXzHeVfxTsqCq4rLuvmL5MTe4QvpSCO15rpFWmKNVBpW0b4cNDUGW864YaKSiRP2E+leCdjC4LPVWPjpK4bJekc68od1xn9LvcEWevSzKYIf8dZNEZxOYtaftne7zQNdSc5Wm49n5IRTIiisSUJgMChR0Z5LTqUcFAVVS1oPdSMN1Krn5fkz6yIGB2Mxmad3DcZHh1F3ohBlwahoySknBiIk2T1MuHs5JiWLTetUz5fy8ti9kmcMpBP5m6Fnuhs92pvD1M92NDej3++t26+8G+x+DhBT45wcgz9iiBYXgzbb7F0TZwUrRC0YIzm5B/1oHWa362wzpolZCYazXtO66RmVUHKqmOopF9UASpVtD4rk5A6Dw/EH1nurg5/8ELOL79lVn5V30brZz1N26yLuL0I78hnGCRNgDgFdzB4gPma/wPRIOiK17LngwEw7lFyoRulyYFXDeSnasuqV6s2g2cAlb0RVctbUumz7utEN40YYkpMdhoN1MP7q9/vk5T/9rl+scYB5jFs/hyUZk3kiOfloHYSXM+L3Cc0F9dt5GW9MS6DGcRkou+xzNVp08crfxVvRQiOHlmkmgFcVgBpUW0soS4mt0n1lypYr6KtWNIAM+lbWqgKpeqhFQ3LyELyZdfxpnYyTnHyZXTqz7DYt8pt19O9a/muMH3Rjyly2xFxrbPpW9hRqJqhUraStEpqWpjKqGqpBDSmZa6+fMGRpl2DdFdlmm30I+tFG1HEOyYI/l96bbfYwQhx8OFyd5GaOxzlmX9DYgDpiUrELc6L6bEf8Sezb3Bd/ZcrgyrvJv2G/Yuj9ZGP6fdxbZw8wkpxsz8e8GPI/b/mK9V3Vf+aFdATm6MnpdDp9BwAA//8DAFsFspX2BAAA

View File

@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUW2/cNhCF/4rAZ0nm/aK31E5Rwwi8iI226NuQHMVEtNKCopwGxv73gtotEvetb8QBZ3j48XDeyAxHJAP5cDp9fMW5rKQlJe0Sp1x3VHaUPTM2KDZI2TNBpXb0L9KS9IDfyUCMDIEFh51AjZ2MVHVejKGLER2jLFKuZO0JX1YyvBFIfcTXFLBf1t8xr2mZyUD+SHNcvtWzIfVhWrbY52XC+3ktMIdq5u7j08Pz46G7lYzePv5y2bniWhv0KVYjMEaUaDvPJOukHG0HVstOj9rICNp6Ji5laS6YZ5j6NX794SEuZcYSBt5z0dOOO3JuSYQC1baHFZ+/n6qTHdNd1dtd3pfDG3nFTAbe/gv00u4mTOlm3fyErzidIK+Yb8JyPMIcSUtOeTlhLgl3NK+Y/bKmUqmGYMBaxqIFI5WJViguTBQUo6VGRRcdaKlHHpQao7fOyFFGzmTUKKU3pCU7vGmCkpa5uXq/nRLOhbTk8ak5TFDGJR/f4X+6EL3/H0CrbTIQQZXQxqAUQoDTnlOMRgetuQ9Azeg5soBUOWkcM5QJx6kTShtrIlAXSEs+QXhJMzb3d2QgVHAHRo6OBsqBKQVgR+u0BCEkOhu8isIxC145bgApeCe1VSCtB80rgdtlLmnelm1t7ueCX/LOggzkV5hWvFD4kMNLKhjKliugP3UN6yEvcQul+REORnvac8pq0VZOW2k+Y0wZQ8GK6jlv137/LdGc0mplyxnn0hygvDS/wfpCBiKD9IwzoNGNgnFrbBAjVYYxwZ1Q3lofx0BHJpnhSjBqdBDOaxMMZZHXAGHNYrPHHzEY5YzrAre8k5HrzoOinUUngXrnR6bfMW4ep1pHBURGfei08bSTIFhnqfUd42oEa4XzYH9Ccg2SrNoD5hmnn678KYW8rMtYmmuimncQPm9zHS3Nnq5vae7+3mnfLeEr5qa+FqS5/qLrA51bckRYt4zHfTINb+RQv1DzvA8oyXsjHG/JU4FcttNVNqp3VPHz+Xz+BwAA//8DAO6X82neBAAA

View File

@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSTT2/cNhDFv4rA81LmP1GkbsnaQQ0jsBEv0qK3ITmKCcvUgqKcBMZ+94LaDVL31osgDDQz7/fm6Y0keEEykA/H480rprKQHSlxKwkmNGWKMn7gfOjEwEVrtZLMmr/JjsQ7/EkG0ivvubdIJWqkKrCOOjl6GgJaznhgolN1JnxbyPBGILYBX6PHdl6+Yl7inMhA/owpzN/rboitn+Y1tHme8DYtBZKvYq5vHu8O9w90rzjb3388f7ngUge0MVQhMAZUaKjjilOlRkPBaEX1qHsVQBvH5bktpoI5wdQu4fm3hjCXhMUPohWyZVRYctqRAAWqbAcLHn4eq5LNputa323l7XV4I6+YySB2vww9j7vyU7x6Wdwap3B1fhZYnivpMc9HzCXi5suGOk1Q4pyay6b9FDEVsiP3j83DBGWc88s7sx7P/Lf/A/9Qt9/8QL8WDPt5TYUMRNULfQb/FBM2t9dkIEwKC70aLfNMAO86ADMaqxVIqdAa77ogLTfgOit6QAbOKm06UMaBFj3Zkf2cSkzrvC7NbSr4LW9sZCCfYFrwTPUh+6dY0Jc1V+C/dBXykOew+tL8Pg1nLWsF47VpLce1NF8wxIy+YEU/5PUy778tWjBWpaw5YyrNA5Sn5g9YniqzV44LDizYUXJheuPlyLqecyms7JwxLoyejVzxXnSSs157aZ3ufc94EIHsCNYkNFv4RgkdOK+pVzxQJaylwJSgyNE5hR1oNO88bu6n2sckBM6cp7p3jCqQnBpmHOWiG8EYaR3Uvl+WXIKhau0Oc8LpX8ifo8/zMo+luSSkeWfClzXVH7vZ0vI9Jvpjc/t69s+Ym3otiKlm+HKg0+l0+gcAAP//AwAV41n7HwQAAA==

View File

@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpSUwW7kKBCGX8XibBzA2Abfsp2MNopGiSatmdXeCihPUBzcApyZKOp3X+HuaCZ7We0FoTJV/uurH95IgGckI7k8HK5fMOREapL9FhJM9JRJyvie87ETIxeN7jkXXfc3qYm/xVcykkFay61G2mKPVDrWUdNOljqHmjPumOhkqQnfExnfCPjG4Yu32CzpK8bkl0BG8s0Ht/wo/wbf2HlZXROXGW9CyhBsEXN1/XC7v7unO8nZ7u6P08mEqRRovCtCYHIoUVHDJadSToqC6iXtp36QDnpleHtK8yFjDDA3yT390uCWHDDbUTSibRgVmhxr4iBDkW0g4f71UJRsmK5KvN7C23Z8Iy8YySjqd6Cnchd29hfPyax+dhenNUN6Sts6gc1LfCU1OcTlgDF73BhdpoTPZn7dQ3r6dDpTtun6J9o1o9sta8hkJK0iNdkYzTNkv4TqLHE3ewx5+5ijD8nb/yrVk5rcPVT3M+Rpic8fRvJwonzzPyB/BvvoA1Y3V2QkrBUaBjlpZpkA3nUAalK6l9C2ErWypnOt5gpMp8UAyMBo2asOpDLQi4HUZLeE7MO6rKm6CRm/x61dMpJPMCc8ab+M9tFntHmNhcFffbHdfVzcanP1a8ycNawRjJekNR/WXH1B5yPajKXBfVzP9f6d0gvGipQ1Rgy5uof8WP0J6ZGMRFppuODAnJ5aLtSgbDuxbuC8FbrtjFLGTZZNXPJBdC1nQ29bbfrBDow74UhNsLiq2ozc6olL0Jr2KDoqtRYUwAgqHNpOuk7qTn9gXN3NJY+14DgzlvaDYVRCy6liylAuugmUarWBYph3JGevyBK7xRhw/q3lz97GJS1Trs4+qD5A+LKG8khUmyd++EB/brSvFvuEsSrTAh/KfTgP6Hg8Hv8BAAD//wMAW9srrmsEAAA=

View File

@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRTXW/UMBD8K5Gf49RfSZy8wRVEVaGraAWIt7W94SxS++Q4BVTdf0fOHSrwxps19u7OzI6fSYBHJCN5dTy+ecKQF1KT7DdIMNFRpijjD5yPrRi5aIZOd3wQX0hN/C3+JCPplbXcDkgldkiVYy01crLUORw4446JVpWe8HUh4zMB3zh88habuHzEtPgYyEg++eDi9zIbfGPnuLomxRlvwpIh2ELm+s397cP+ju4UZ7v96/PLBZfSoPGuEIHJoUJNDVecKjVpCrpTtJu6XjnotOHyXOZDxhRgbhb37YWDizlgtqNohGwYFQM51cRBhkLbwIIPP4+FyWbTdcHrDd6O4zN5wkRGUf829Nzuys7+ysbHRwjuavLBLwdSk2OKR0zZ42bJpnKeIfsYqsuQ3ewxZFKT/X11N0OeYnr8y6f7s/Sb/1D+HuzBB6xurslImBQD9GoamGUCeNsC6EkPnQIpFQ7amtbJgWsw7SB6QAZmUJ1uQWkDnehJTXYxZB/WuC7VTcj4NW0KyEjewrzgmfurZA8+o81rKrI+dyUL+MPnXXQF4KQmdym61ebqZRWcNawRrFzu13xcc/UBnU9oMxa9D2m9tP+3pBOMFWZrShhydQf5UL2D5UBGoqwyXHBgbpgkF7rXVk6s7TmXYpCt0dq4ybKJK96LVnLWd1YOputtz7gTrvAum6+2sIkJUSvOqTM9o2pQhoKwmk5SSuEc8gmKRS+WV/u51DEJjjNjadcbRhVITjXThnLRTqC1HAzoPyy5pEEV7BZTwPkPye+9TXGJU64usaj+MuHDGspHrraIfPeB/tjMv472G6aqLA98KJm97Ot0Op1+AQAA//8DAMC59voPBAAA

View File

@@ -0,0 +1,5 @@
https://dc.services.visualstudio.com/v2/track
Content-Type:application/x-json-stream
Content-Encoding:gzip
H4sIAAAAAAAACpRUXU/lNhD9K5FfexP8/ZE3FlgVrVjQctVWfRvbE3AJzpXj7HaF+O+rBOgCVR/6Ylkjz8yZc47ngWS4R9KT48Ph7CvmOpMdqWkLccp1S2VL2Z6xXvGe8c5pK4TRf5IdSZ/wO+mJkSGw4LAVqLGVkarWiyG0MaJjlEXKlVxrws1M+gcCqYv4NQXspvk3LHOaMunJ7ynH6dvaG1IXxmmJXZlGPM9zhRxWMKdn15/2l1ftiWT05PLD08sZ57VAl+IKBIaIEm3rmWStlINtwWrZ6kEbGUFbz8RTWsoVS4axm+PdTwxxqhlr6HnHRUdb7sjjjkSosML2MOP++2FFstF0usZ3W3i79g/kKxbS890LoU/ljsKYju5nv6QxHm0n2ZFDmQ5YasKNkW3IcYSaptw89zgZE+ZKduTDmnK9hIDzTHryEcYZX8Jn+SZl/FjgHr9N5e7zU9/u89me7MhVmf7CUK+g3pKeMBNA26ADHzRnig2Keelx0EIPnDGmQnB+cE4BR8qtN2gdVV5THpzyzpEdubxurkaow1Tu3wh2/aTB+f+Q4Pr45CyDHzG+H2kP5QYr6YnSyqMYLNdeQwQn7MAAhYvBC63QehMtGiO4gKACtSwwwxQ4o5TRgjOyIxcQblPG5vyU9IQK7sDIwdFAOTClAOxgnZYghERng1dROGbBK8cNIAXvpLYKpPWguSE7cjLlmvIyLXNznivelE2xVxNcXjfHJdymiqEuZdXiD71a/6pMcQm1+Wk1RjvacbqivFzqYanNF4ypYKgbJfuyPNd7n6I5pSuUpRTMtVnVbX6FeZVYBukZZ0CjGwTj1tggBqoMY4I7oby1Pg6BDkwyw5Vg1OggnNcmGMoiX32Jq7Ob7TPpIXAwVLcyRNtKR0ML3EMbKYQoBQt2MG84bi7HNY8KiIz60GrjaStBsNZS61vG1QDWCufBvvXvqxFtJzraMSU5f0Xb85eQa94nLBnHV7RcpFCmeRpq8+zI5g1Rr/qcpvkwwvf37dqCI8KMLddMiZZx/ouMgnIRUb8UOLnFcPdvx35Z8ropm83631Ju/97kPp3CHZZmtQukvC6FfzIulrGm/W1BiBgvpojviz7uyD3CvBS833bxth4ylg3G6fJkufN8kcYxzRimHGfSc2uc6qix+hnvfz90jHeMSf74+Pj4AwAA//8DAJUznJr7BQAA

View File

@@ -29,6 +29,8 @@ public sealed class TelemetryUpdateSnapshot
public required IReadOnlyList<PumpControlSnapshot> PumpControls { get; init; }
public required IReadOnlyList<ValveControlSnapshot> ValveControls { get; init; }
public required IReadOnlyList<AlarmMessage> Alarms { get; init; }
public double? ProximalPressureRawKpa { get; init; }
public double? DistalPressureRawKpa { get; init; }
public required bool IsLiveConnected { get; init; }
public required string EndpointDescription { get; init; }
public required DateTime? LastSuccessfulReadAt { get; init; }

View File

@@ -11,10 +11,11 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
private const string IpAddress = "192.168.1.10";
private const int Port = 502;
private const byte SlaveId = 1;
// Keep distinct pressure registers until the distal PLC address is confirmed.
private const ushort ProximalPressureRegister = 1330;
private const ushort DistalPressureRegister = 1380;
private const double FlowRegisterScale = 0.01d;
private const double KpaToMmHg = 7.50061683d;
private const ushort PressureRegisterBlockLength = (ushort)(DistalPressureRegister - ProximalPressureRegister + 2);
private const int SmoothingWindowSize = 5;
private static readonly TimeSpan ConnectionAttemptTimeout = TimeSpan.FromMilliseconds(300);
private static readonly TimeSpan ConnectionRetryInterval = TimeSpan.FromSeconds(5);
@@ -88,6 +89,10 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
private bool _connectionInitialized;
private Task? _connectionTask;
private DateTime _nextConnectionAttemptUtc = DateTime.MinValue;
private double? _proximalPressureRawKpa;
private double? _distalPressureRawKpa;
private double? _proximalPressureDecodedKpa;
private double? _distalPressureDecodedKpa;
private int HighestConfiguredCoilAddress => Math.Max(
_pumpControls.Max(item => item.StartAddress),
@@ -308,19 +313,33 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
{
if (_master is null || !liveReadSucceeded)
{
_proximalPressureRawKpa = null;
_distalPressureRawKpa = null;
_proximalPressureDecodedKpa = null;
_distalPressureDecodedKpa = null;
SimulatePressureChannels();
return;
}
try
{
var proximalRaw = _master.ReadHoldingRegisters(SlaveId, ProximalPressureRegister, 1)[0];
var distalRaw = _master.ReadHoldingRegisters(SlaveId, DistalPressureRegister, 1)[0];
SetSmoothedValue("近端压力", ConvertRegisterToPressure(proximalRaw, Channel("近端压力")));
SetSmoothedValue("远端压力", ConvertRegisterToPressure(distalRaw, Channel("远端压力")));
var pressureRegisters = _master.ReadHoldingRegisters(SlaveId, ProximalPressureRegister, PressureRegisterBlockLength);
var distalOffset = DistalPressureRegister - ProximalPressureRegister;
var proximalRawKpa = ConvertRegistersToPressureKpa(pressureRegisters[0], pressureRegisters[1]);
var distalRawKpa = ConvertRegistersToPressureKpa(pressureRegisters[distalOffset], pressureRegisters[distalOffset + 1]);
_proximalPressureRawKpa = proximalRawKpa;
_distalPressureRawKpa = distalRawKpa;
_proximalPressureDecodedKpa = proximalRawKpa;
_distalPressureDecodedKpa = distalRawKpa;
SetSmoothedValue("近端压力", ConvertPressureKpaToMmHg(proximalRawKpa));
SetSmoothedValue("远端压力", ConvertPressureKpaToMmHg(distalRawKpa));
}
catch
{
_proximalPressureRawKpa = null;
_distalPressureRawKpa = null;
_proximalPressureDecodedKpa = null;
_distalPressureDecodedKpa = null;
ReleaseConnection();
_nextConnectionAttemptUtc = DateTime.MinValue;
SimulatePressureChannels();
@@ -468,6 +487,8 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
Message = alarm.Message
})
.ToList(),
ProximalPressureRawKpa = _proximalPressureRawKpa,
DistalPressureRawKpa = _distalPressureRawKpa,
IsLiveConnected = _master is not null && _tcpClient?.Connected == true,
EndpointDescription = EndpointDescription,
LastSuccessfulReadAt = null,
@@ -477,7 +498,9 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
private void SetSmoothedValue(string channelName, double nextValue)
{
var channel = Channel(channelName);
var clampedValue = Math.Clamp(nextValue, channel.Min, channel.Max);
var clampedValue = ShouldClampChannelValue(channelName)
? Math.Clamp(nextValue, channel.Min, channel.Max)
: nextValue;
var window = Window(channelName);
window.Enqueue(clampedValue);
@@ -541,7 +564,9 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
private void SetChannelValueDirect(string channelName, double nextValue)
{
var channel = Channel(channelName);
var clampedValue = Math.Clamp(nextValue, channel.Min, channel.Max);
var clampedValue = ShouldClampChannelValue(channelName)
? Math.Clamp(nextValue, channel.Min, channel.Max)
: nextValue;
var window = Window(channelName);
window.Clear();
window.Enqueue(clampedValue);
@@ -571,14 +596,44 @@ public sealed class MockModbusTelemetryService : IModbusTelemetryService, IDispo
private static double ConvertRegisterToFlow(ushort rawValue) => rawValue * FlowRegisterScale;
private static double ConvertRegisterToPressure(ushort rawValue, DeviceChannel channel)
private static double ConvertRegistersToPressureKpa(ushort firstWord, ushort secondWord)
{
var signedValue = rawValue > short.MaxValue ? rawValue - 65536 : rawValue;
return Math.Clamp(signedValue, channel.Min, channel.Max);
var lowWordFirst = DecodeFloat(firstWord, secondWord, lowWordFirst: true);
var highWordFirst = DecodeFloat(firstWord, secondWord, lowWordFirst: false);
var lowWordFirstValid = IsPlausiblePressureKpa(lowWordFirst);
var highWordFirstValid = IsPlausiblePressureKpa(highWordFirst);
if (lowWordFirstValid && !highWordFirstValid)
{
return lowWordFirst;
}
if (!lowWordFirstValid && highWordFirstValid)
{
return highWordFirst;
}
return lowWordFirst;
}
private static double ConvertPressureKpaToMmHg(double pressureKpa) => pressureKpa * KpaToMmHg;
private static float DecodeFloat(ushort firstWord, ushort secondWord, bool lowWordFirst)
{
var bits = lowWordFirst
? ((uint)secondWord << 16) | firstWord
: ((uint)firstWord << 16) | secondWord;
return BitConverter.Int32BitsToSingle(unchecked((int)bits));
}
private static bool IsPlausiblePressureKpa(float value) =>
!float.IsNaN(value) && !float.IsInfinity(value) && value is > -1000f and < 1000f;
private PumpControlChannel Pump(string key) => _pumpControls.First(pump => pump.Key == key);
private static bool ShouldClampChannelValue(string channelName) =>
channelName is not "近端压力" and not "远端压力";
private DeviceChannel Channel(string name) => _channels.First(channel => channel.Name == name);
private double Next(double min, double max) => min + (_random.NextDouble() * (max - min));

View File

@@ -15,6 +15,8 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
private const ushort ProximalPressureRegister = 1330;
private const ushort DistalPressureRegister = 1380;
private const double FlowRegisterScale = 0.01d;
private const double KpaToMmHg = 7.50061683d;
private const ushort PressureRegisterBlockLength = (ushort)(DistalPressureRegister - ProximalPressureRegister + 2);
private static readonly TimeSpan ConnectionAttemptTimeout = TimeSpan.FromMilliseconds(300);
private static readonly TimeSpan ConnectionRetryInterval = TimeSpan.FromSeconds(5);
@@ -88,6 +90,10 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
private Task? _connectionTask;
private DateTime _nextConnectionAttemptUtc = DateTime.MinValue;
private DateTime? _lastSuccessfulReadAt;
private double? _proximalPressureRawKpa;
private double? _distalPressureRawKpa;
private double? _proximalPressureDecodedKpa;
private double? _distalPressureDecodedKpa;
private string _lastErrorMessage = "等待首次连接";
public ModbusTelemetryService()
@@ -305,6 +311,12 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
if (_master is null)
{
ApplyUnavailableDeviceState();
_proximalPressureRawKpa = null;
_distalPressureRawKpa = null;
_proximalPressureDecodedKpa = null;
_distalPressureDecodedKpa = null;
_proximalPressureDecodedKpa = null;
_distalPressureDecodedKpa = null;
return false;
}
@@ -358,6 +370,8 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
{
if (_master is null || !liveReadSucceeded)
{
_proximalPressureRawKpa = null;
_distalPressureRawKpa = null;
SetChannelAvailability("近端压力", false);
SetChannelAvailability("远端压力", false);
return false;
@@ -365,10 +379,16 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
try
{
var proximalRaw = _master.ReadHoldingRegisters(_slaveId, ProximalPressureRegister, 1)[0];
var distalRaw = _master.ReadHoldingRegisters(_slaveId, DistalPressureRegister, 1)[0];
SetChannelValue("近端压力", ConvertRegisterToPressure(proximalRaw, Channel("近端压力")), true);
SetChannelValue("远端压力", ConvertRegisterToPressure(distalRaw, Channel("远端压力")), true);
var pressureRegisters = _master.ReadHoldingRegisters(_slaveId, ProximalPressureRegister, PressureRegisterBlockLength);
var distalOffset = DistalPressureRegister - ProximalPressureRegister;
var proximalRawKpa = ConvertRegistersToPressureKpa(pressureRegisters[0], pressureRegisters[1]);
var distalRawKpa = ConvertRegistersToPressureKpa(pressureRegisters[distalOffset], pressureRegisters[distalOffset + 1]);
_proximalPressureRawKpa = proximalRawKpa;
_distalPressureRawKpa = distalRawKpa;
_proximalPressureDecodedKpa = proximalRawKpa;
_distalPressureDecodedKpa = distalRawKpa;
SetChannelValue("近端压力", ConvertPressureKpaToMmHg(proximalRawKpa), true);
SetChannelValue("远端压力", ConvertPressureKpaToMmHg(distalRawKpa), true);
return true;
}
catch (Exception ex)
@@ -486,6 +506,8 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
Message = alarm.Message
})
.ToList(),
ProximalPressureRawKpa = _proximalPressureRawKpa,
DistalPressureRawKpa = _distalPressureRawKpa,
IsLiveConnected = _master is not null && _tcpClient?.Connected == true,
EndpointDescription = EndpointDescription,
LastSuccessfulReadAt = _lastSuccessfulReadAt,
@@ -523,6 +545,10 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
{
ReleaseConnection();
_nextConnectionAttemptUtc = DateTime.MinValue;
_proximalPressureRawKpa = null;
_distalPressureRawKpa = null;
_proximalPressureDecodedKpa = null;
_distalPressureDecodedKpa = null;
_lastErrorMessage = errorMessage;
ApplyUnavailableDeviceState();
}
@@ -530,7 +556,9 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
private void SetChannelValue(string channelName, double nextValue, bool isAvailable)
{
var channel = Channel(channelName);
channel.Value = Math.Clamp(nextValue, channel.Min, channel.Max);
channel.Value = ShouldClampChannelValue(channelName)
? Math.Clamp(nextValue, channel.Min, channel.Max)
: nextValue;
channel.IsAvailable = isAvailable;
}
@@ -565,11 +593,41 @@ public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposabl
private static double ConvertRegisterToFlow(ushort rawValue) => rawValue * FlowRegisterScale;
private static double ConvertRegisterToPressure(ushort rawValue, DeviceChannel channel)
private static double ConvertRegistersToPressureKpa(ushort firstWord, ushort secondWord)
{
var signedValue = rawValue > short.MaxValue ? rawValue - 65536 : rawValue;
return Math.Clamp(signedValue, channel.Min, channel.Max);
var lowWordFirst = DecodeFloat(firstWord, secondWord, lowWordFirst: true);
var highWordFirst = DecodeFloat(firstWord, secondWord, lowWordFirst: false);
var lowWordFirstValid = IsPlausiblePressureKpa(lowWordFirst);
var highWordFirstValid = IsPlausiblePressureKpa(highWordFirst);
if (lowWordFirstValid && !highWordFirstValid)
{
return lowWordFirst;
}
if (!lowWordFirstValid && highWordFirstValid)
{
return highWordFirst;
}
return lowWordFirst;
}
private static double ConvertPressureKpaToMmHg(double pressureKpa) => pressureKpa * KpaToMmHg;
private static float DecodeFloat(ushort firstWord, ushort secondWord, bool lowWordFirst)
{
var bits = lowWordFirst
? ((uint)secondWord << 16) | firstWord
: ((uint)firstWord << 16) | secondWord;
return BitConverter.Int32BitsToSingle(unchecked((int)bits));
}
private static bool IsPlausiblePressureKpa(float value) =>
!float.IsNaN(value) && !float.IsInfinity(value) && value is > -1000f and < 1000f;
private static bool ShouldClampChannelValue(string channelName) =>
channelName is not "近端压力" and not "远端压力";
private DeviceChannel Channel(string name) => _channels.First(channel => channel.Name == name);
}

View File

@@ -3,21 +3,25 @@ using System.ComponentModel;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Windows.Threading;
using Cardiopulmonarybypasssystems.Models;
using Cardiopulmonarybypasssystems.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using QuestPDF.Fluent;
namespace Cardiopulmonarybypasssystems.ViewModels;
public partial class MainViewModel : ObservableObject, IDisposable
{
private readonly IStandardRepository _repository;
private readonly IModbusTelemetryService _telemetryService;
private readonly DispatcherTimer _timer;
private const double AntiCollapseTargetNegativePressure = -6.67;
private const double PressureKpaToMmHg = 7.50061683d;
private const int TrendHistoryCapacity = 60;
private static readonly string LimitSettingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
@@ -39,6 +43,8 @@ public partial class MainViewModel : ObservableObject, IDisposable
private DateTime? _telemetryLastUpdatedAt;
private string _telemetryStatusDetail = string.Empty;
private string _lastTelemetryRefreshFailureMessage = string.Empty;
private double? _proximalPressureRawKpa;
private double? _distalPressureRawKpa;
[ObservableProperty]
private string pageTitle = "心肺转流系统一次性使用动静脉插管检测";
@@ -140,6 +146,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
public MainViewModel(IStandardRepository repository, IModbusTelemetryService telemetryService)
{
_repository = repository;
_telemetryService = telemetryService;
_isTelemetryOnline = telemetryService.IsLiveConnected;
_plcEndpointDisplay = telemetryService.EndpointDescription;
@@ -339,8 +346,8 @@ public partial class MainViewModel : ObservableObject, IDisposable
};
public double PressureTrendMax => MaxTrendValue([ProximalPressureTrendValues, DistalPressureTrendValues, DeltaPressureTrendValues], 40d);
public double FlowTrendMax => MaxTrendValue([ActiveFlowTrendPrimaryValues, ActiveFlowTrendSecondaryValues, ActiveFlowTrendTertiaryValues], Math.Max(RatedMaxFlow, 1d));
public string ProximalPressureDisplay => ChannelDisplay("近端压力", "F1", "mmHg");
public string DistalPressureDisplay => ChannelDisplay("远端压力", "F1", "mmHg");
public string ProximalPressureDisplay => PressureDisplay("近端压力", _proximalPressureRawKpa);
public string DistalPressureDisplay => PressureDisplay("远端压力", _distalPressureRawKpa);
public string FlowImbalanceDisplay => HasChannelTelemetry("主泵流量", "动脉回输流量") ? $"{Math.Abs(PumpFlow - ReturnFlow):F2} L/min" : "--";
public string PumpFlowLoadDisplay => ChannelLoadDisplay("主泵流量");
public string DrainageFlowLoadDisplay => ChannelLoadDisplay("静脉引流流量");
@@ -918,7 +925,7 @@ public partial class MainViewModel : ObservableObject, IDisposable
[RelayCommand]
private void ExportReport()
{
ExportReportExcel();
ExportReportExcelWithDialog();
}
private void ExportReportExcel()
@@ -974,6 +981,69 @@ public partial class MainViewModel : ObservableObject, IDisposable
}
}
private void ExportReportExcelWithDialog()
{
if (!CanExportReport)
{
LatestAction = "检测尚未完成,不能导出正式的 Excel 检查报告。";
return;
}
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss");
var batchToken = string.IsNullOrWhiteSpace(BatchNumber) ? "未填写批号" : SanitizeFileNameSegment(BatchNumber.Trim());
var excelPath = PromptReportSavePath($"检查报告{batchToken}-{timestamp}.xlsx");
if (string.IsNullOrWhiteSpace(excelPath))
{
LatestAction = "已取消导出报表。";
return;
}
try
{
var document = new ExcelReportDocument(
pageTitle: PageTitle,
batchNumber: BatchNumber,
currentStage: CurrentStage,
operatorName: OperatorName,
reviewerName: ReviewerName,
approverName: ApproverName,
complianceDisplay: ComplianceDisplay,
deltaPressureDisplay: DeltaPressureDisplay,
detectionSummary: DetectionSummary,
configurationSummary: ConfigurationSummary,
exportTime: DateTime.Now,
inspectionItems: InspectionItems.ToList(),
traceEvents: TraceEvents.ToList(),
kinkResistanceEntries: KinkResistanceEntries.ToList(),
kinkResistanceFlowPointDisplay: KinkResistanceFlowPointDisplay,
kinkResistanceMandrelDiameterDisplay: KinkResistanceMandrelDiameterDisplay,
pressureDropEntries: PressureDropEntries.ToList(),
pressureDropLimitDisplay: PressureDropLimitDisplay,
antiCollapseBaselineDisplay: AntiCollapseBaselineDisplay,
antiCollapseComparisonDisplay: AntiCollapseComparisonDisplay,
antiCollapseCurrentNegativePressure: NegativeAssistPressureDisplay,
antiCollapseCurrentFlowDisplay: PumpFlowDisplay,
antiCollapseAllowedIncreaseRateDisplay: $"{AntiCollapseAllowedIncreaseRate:F1}%",
recirculationEntries: RecirculationEntries.ToList(),
recirculationLimitDisplay: RecirculationLimitDisplay);
document.GenerateExcel(excelPath);
LatestAction = $"已导出 Excel 检查报告 {excelPath}";
TraceEvents.Insert(0, NewTrace("Excel检查报告导出", Path.GetFileName(excelPath)));
MessageBox.Show(
$"报表已导出成功:\n{excelPath}\n\n点击“确定”后页面将恢复初始状态。",
"导出成功",
MessageBoxButton.OK,
MessageBoxImage.Information);
ResetSessionAfterExport();
}
catch (Exception ex)
{
LatestAction = $"Excel 检查报告导出失败:{ex.Message}";
TraceEvents.Insert(0, NewTrace("Excel检查报告导出失败", ex.Message));
}
}
private void ExportReportPdf()
{
var outputDirectory = ResolveReportOutputDirectory();
@@ -1537,6 +1607,16 @@ public partial class MainViewModel : ObservableObject, IDisposable
_ => $"主泵 {PressureDropPumpFlowDisplay} / 流量偏差 {FlowImbalanceDisplay}"
};
private string PressureDisplay(string channelName, double? rawKpa)
{
if (rawKpa.HasValue)
{
return $"{rawKpa.Value * PressureKpaToMmHg:F1} mmHg";
}
return ChannelDisplay(channelName, "F1", "mmHg");
}
private static string FormatHemolysisDate(DateTime? value) => value?.ToString("yyyy-MM-dd") ?? string.Empty;
private static string FormatHemolysisValue(double? value, string format) => value.HasValue ? value.Value.ToString(format) : string.Empty;
@@ -2419,6 +2499,8 @@ public partial class MainViewModel : ObservableObject, IDisposable
_plcEndpointDisplay = snapshot.EndpointDescription;
_telemetryLastUpdatedAt = snapshot.LastSuccessfulReadAt;
_telemetryStatusDetail = snapshot.LastErrorMessage;
_proximalPressureRawKpa = snapshot.ProximalPressureRawKpa;
_distalPressureRawKpa = snapshot.DistalPressureRawKpa;
foreach (var channelSnapshot in snapshot.Channels)
{
@@ -2552,6 +2634,188 @@ public partial class MainViewModel : ObservableObject, IDisposable
private async void OnTelemetryTimerTick(object? sender, EventArgs e) => await RefreshTelemetryAsync();
private static string? PromptReportSavePath(string defaultFileName)
{
var dialog = new SaveFileDialog
{
Title = "导出检查报表",
Filter = "Excel 工作簿 (*.xlsx)|*.xlsx",
DefaultExt = ".xlsx",
AddExtension = true,
OverwritePrompt = true,
ValidateNames = true,
InitialDirectory = ResolveReportOutputDirectory(),
FileName = defaultFileName
};
return dialog.ShowDialog() == true ? dialog.FileName : null;
}
private void ResetSessionAfterExport()
{
DetectionCompleted = false;
CurrentStage = "检测进行中";
AcquisitionRunning = true;
OperatorName = Environment.UserName;
ReviewerName = string.Empty;
ApproverName = string.Empty;
BatchNumber = string.Empty;
ItemSearchText = string.Empty;
ActiveFilter = ItemFilterOptions.FirstOrDefault() ?? "全部";
ResetInspectionItems();
ResetSpecializedEntries();
ResetHemolysisParameters();
ResetHemolysisSamplingEntries();
ResetTransientSessionState();
ResetTraceAndAlarmState();
ClearTrendSeries(
ProximalPressureTrendValues,
DistalPressureTrendValues,
DeltaPressureTrendValues,
PressureDropPumpTrendValues,
RecirculationMainPumpTrendValues,
RecirculationReturnPumpTrendValues,
RecirculationDrainagePumpTrendValues,
KinkResistancePumpTrendValues,
HemolysisDrainageSingleTrendValues,
HemolysisReturnSingleTrendValues,
HemolysisDualLumenTrendValues);
RaiseTrendPropertyChanges();
RefreshSpecializedJudgements();
RefreshTelemetryPanel();
RefreshComputedState();
RefreshFilteredItemsView();
RefreshDeviceStatus();
LatestAction = "系统已加载标准项目,等待 PLC 实时数据。";
TraceEvents.Insert(0, NewTrace("任务初始化", $"已加载 {InspectionItems.Count} 项检测标准,实时端点 {_telemetryService.EndpointDescription}"));
if (!_timer.IsEnabled)
{
_timer.Start();
}
_ = RefreshTelemetryAsync();
}
private void ResetInspectionItems()
{
SelectedItem = null;
InspectionItems.Clear();
foreach (var item in _repository.GetInspectionItems())
{
InspectionItems.Add(item);
}
}
private void ResetSpecializedEntries()
{
foreach (var entry in PressureDropEntries)
{
entry.ActualPumpFlow = 0d;
entry.ProximalPressure = 0d;
entry.DistalPressure = 0d;
entry.Temperature = 0d;
entry.SampledAt = null;
}
foreach (var entry in KinkResistanceEntries)
{
entry.BaselineFlow = 0d;
entry.KinkedFlow = 0d;
entry.Temperature = 0d;
entry.BaselineCapturedAt = null;
entry.KinkedCapturedAt = null;
}
foreach (var entry in RecirculationEntries)
{
entry.ActualPumpFlow = 0d;
entry.DrainageFlow = 0d;
entry.ReturnFlow = 0d;
entry.OnlineEstimate = 0d;
entry.Temperature = 0d;
entry.ConcentrationC1 = null;
entry.ConcentrationC2 = null;
entry.SampledAt = null;
}
}
private void ResetHemolysisParameters()
{
HemolysisTestParameters.BloodSource = "肝素化牛血";
HemolysisTestParameters.CollectionDate = null;
HemolysisTestParameters.Anticoagulant = "肝素";
HemolysisTestParameters.InitialHematocrit = null;
HemolysisTestParameters.AdjustedHematocrit = 0.30;
HemolysisTestParameters.Glucose = 10;
HemolysisTestParameters.TotalHemoglobin = 12;
HemolysisTestParameters.InitialFreeHemoglobin = null;
HemolysisTestParameters.CircuitPrimingVolume = null;
HemolysisTestParameters.CircuitVolumeDifference = null;
HemolysisTestParameters.SetFlow = RatedMaxFlow;
HemolysisTestParameters.RunTimeMinutes = 360;
HemolysisTestParameters.TargetTemperature = 37;
}
private void ResetHemolysisSamplingEntries()
{
foreach (var entry in HemolysisSamplingEntries)
{
entry.ClockTime = string.Empty;
entry.FreeHemoglobin = null;
entry.Hematocrit = null;
entry.WhiteCellCount = null;
entry.PlateletCount = null;
entry.Hemoglobin = null;
entry.Flow = null;
entry.Pressure = null;
entry.Temperature = entry.Sequence == 1 ? 37.0 : null;
entry.Remarks = entry.Sequence switch
{
1 => "背景值",
3 or 4 or 6 or 7 => "过程观察",
8 => "试验完成",
_ => string.Empty
};
}
}
private void ResetTransientSessionState()
{
_antiCollapseBaselinePressureDrop = null;
_antiCollapseBaselineFlow = null;
_antiCollapseBaselineCapturedAt = null;
_lastAutoAntiCollapseResult = string.Empty;
_lastAutoAntiCollapseNote = string.Empty;
_lastAutoRecirculationResult = string.Empty;
_lastAutoRecirculationNote = string.Empty;
_lastAutoHemolysisResult = string.Empty;
_lastAutoHemolysisNote = string.Empty;
ResultValue = string.Empty;
ResultNote = string.Empty;
ResultOperator = OperatorName;
SelectedResultStatusText = "待检";
OnPropertyChanged(nameof(HasAntiCollapseBaseline));
OnPropertyChanged(nameof(AntiCollapseBaselineDisplay));
OnPropertyChanged(nameof(AntiCollapseComparisonDisplay));
}
private void ResetTraceAndAlarmState()
{
TraceEvents.Clear();
foreach (var trace in _repository.GetInitialTraceEvents())
{
TraceEvents.Add(trace);
}
AlarmMessages.Clear();
OnPropertyChanged(nameof(AlarmSummaryDisplay));
}
private static string ResolveReportOutputDirectory()
{
foreach (var folder in new[]