From cefe5ee26ca80744d9ccc87e8342df9f8851caab Mon Sep 17 00:00:00 2001 From: Rice <1840169763@qq.com> Date: Wed, 15 Apr 2026 09:52:33 +0800 Subject: [PATCH] init --- .gitignore | 1 + go.mod | 81 ++ go.sum | 499 +++++++++++ internal/core/internal/constant.go | 78 ++ internal/core/internal/cutter.go | 392 +++++++++ internal/core/internal/zap_core.go | 74 ++ internal/core/server_run.go | 71 ++ internal/core/zap.go | 38 + internal/global/global.go | 47 + internal/global/model.go | 13 + internal/initialize/init.go | 43 + internal/initialize/timer.go | 49 ++ internal/library/ADL200/cmd.go | 156 ++++ internal/library/ADL200/cmdBuild.go | 147 ++++ internal/library/ADL200/cmdOperate.go | 55 ++ internal/library/ADL400/cmd.go | 129 +++ internal/library/ADL400/cmdBuild.go | 146 ++++ internal/library/ADL400/cmdOperate.go | 53 ++ internal/library/m9z/m9z.go | 74 ++ internal/library/m9z/m9z_Bluetooth.go | 120 +++ internal/library/m9z/m9z_DeviceConfig.go | 99 +++ internal/library/m9z/m9z_DeviceOutStatus.go | 39 + internal/library/m9z/m9z_DeviceStatus.go | 60 ++ internal/library/m9z/m9z_DeviceTime.go | 43 + internal/library/m9z/m9z_Firmware.go | 411 +++++++++ internal/library/m9z/m9z_LgnLat.go | 103 +++ internal/library/m9z/m9z_Manual.go | 257 ++++++ internal/library/m9z/m9z_Mode.go | 27 + internal/library/m9z/m9z_RelayStatus.go | 36 + internal/library/m9z/m9z_Restart.go | 13 + internal/library/m9z/m9z_Save.go | 17 + internal/library/m9z/m9z_SubLoopParameters.go | 123 +++ internal/library/m9z/m9z_SunRiseSet.go | 69 ++ internal/library/m9z/m9z_SunRiseTime.go | 43 + internal/library/m9z/m9z_Task.go | 139 +++ internal/library/m9z/m9z_Task_Plugin.go | 674 ++++++++++++++ internal/library/m9z/m9z_Version.go | 43 + internal/library/m9z/m9z_cmdOperate.go | 322 +++++++ .../library/meterControl/Adl200Impl/init.go | 21 + .../library/meterControl/Adl200Impl/read.go | 304 +++++++ .../library/meterControl/Adl200Impl/write.go | 168 ++++ .../meterControl/Adl400Impl/adl400Impl.go | 4 + .../library/meterControl/Adl400Impl/init.go | 15 + .../library/meterControl/Adl400Impl/read.go | 355 ++++++++ .../library/meterControl/Adl400Impl/write.go | 150 ++++ internal/library/meterControl/init.go | 50 ++ .../library/meterControl/meter_interface.go | 49 ++ internal/model/config/config.go | 150 ++++ internal/model/config/zap.go | 99 +++ internal/module/adl400TtyApi/init.go | 48 + internal/module/adl400TtyApi/read.go | 308 +++++++ internal/module/adl400TtyApi/write.go | 23 + .../basicInfo/deviceManagement/meter.go | 46 + .../deviceManagement/tgk_main_model.go | 30 + .../module/license_terminal/license.model.go | 26 + .../license_terminal/licenseController.go | 15 + .../license_terminal.package.go | 820 ++++++++++++++++++ internal/module/m9zTtyApi/cron.go | 175 ++++ internal/module/m9zTtyApi/falt.go | 20 + internal/module/m9zTtyApi/init.go | 128 +++ internal/module/m9zTtyApi/read.go | 410 +++++++++ internal/module/m9zTtyApi/set.go | 366 ++++++++ internal/module/m9zTtyPwd/init.go | 367 ++++++++ .../mainControllerClient.go | 246 ++++++ .../module/maincontrollerClient/serialport.go | 246 ++++++ utility/directory.go | 193 +++++ utility/paramsCheck.go | 107 +++ utility/timer/timed_task.go | 237 +++++ 68 files changed, 9960 insertions(+) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/core/internal/constant.go create mode 100644 internal/core/internal/cutter.go create mode 100644 internal/core/internal/zap_core.go create mode 100644 internal/core/server_run.go create mode 100644 internal/core/zap.go create mode 100644 internal/global/global.go create mode 100644 internal/global/model.go create mode 100644 internal/initialize/init.go create mode 100644 internal/initialize/timer.go create mode 100644 internal/library/ADL200/cmd.go create mode 100644 internal/library/ADL200/cmdBuild.go create mode 100644 internal/library/ADL200/cmdOperate.go create mode 100644 internal/library/ADL400/cmd.go create mode 100644 internal/library/ADL400/cmdBuild.go create mode 100644 internal/library/ADL400/cmdOperate.go create mode 100644 internal/library/m9z/m9z.go create mode 100644 internal/library/m9z/m9z_Bluetooth.go create mode 100644 internal/library/m9z/m9z_DeviceConfig.go create mode 100644 internal/library/m9z/m9z_DeviceOutStatus.go create mode 100644 internal/library/m9z/m9z_DeviceStatus.go create mode 100644 internal/library/m9z/m9z_DeviceTime.go create mode 100644 internal/library/m9z/m9z_Firmware.go create mode 100644 internal/library/m9z/m9z_LgnLat.go create mode 100644 internal/library/m9z/m9z_Manual.go create mode 100644 internal/library/m9z/m9z_Mode.go create mode 100644 internal/library/m9z/m9z_RelayStatus.go create mode 100644 internal/library/m9z/m9z_Restart.go create mode 100644 internal/library/m9z/m9z_Save.go create mode 100644 internal/library/m9z/m9z_SubLoopParameters.go create mode 100644 internal/library/m9z/m9z_SunRiseSet.go create mode 100644 internal/library/m9z/m9z_SunRiseTime.go create mode 100644 internal/library/m9z/m9z_Task.go create mode 100644 internal/library/m9z/m9z_Task_Plugin.go create mode 100644 internal/library/m9z/m9z_Version.go create mode 100644 internal/library/m9z/m9z_cmdOperate.go create mode 100644 internal/library/meterControl/Adl200Impl/init.go create mode 100644 internal/library/meterControl/Adl200Impl/read.go create mode 100644 internal/library/meterControl/Adl200Impl/write.go create mode 100644 internal/library/meterControl/Adl400Impl/adl400Impl.go create mode 100644 internal/library/meterControl/Adl400Impl/init.go create mode 100644 internal/library/meterControl/Adl400Impl/read.go create mode 100644 internal/library/meterControl/Adl400Impl/write.go create mode 100644 internal/library/meterControl/init.go create mode 100644 internal/library/meterControl/meter_interface.go create mode 100644 internal/model/config/config.go create mode 100644 internal/model/config/zap.go create mode 100644 internal/module/adl400TtyApi/init.go create mode 100644 internal/module/adl400TtyApi/read.go create mode 100644 internal/module/adl400TtyApi/write.go create mode 100644 internal/module/basicInfo/deviceManagement/meter.go create mode 100644 internal/module/basicInfo/deviceManagement/tgk_main_model.go create mode 100644 internal/module/license_terminal/license.model.go create mode 100644 internal/module/license_terminal/licenseController.go create mode 100644 internal/module/license_terminal/license_terminal.package.go create mode 100644 internal/module/m9zTtyApi/cron.go create mode 100644 internal/module/m9zTtyApi/falt.go create mode 100644 internal/module/m9zTtyApi/init.go create mode 100644 internal/module/m9zTtyApi/read.go create mode 100644 internal/module/m9zTtyApi/set.go create mode 100644 internal/module/m9zTtyPwd/init.go create mode 100644 internal/module/maincontrollerClient/mainControllerClient.go create mode 100644 internal/module/maincontrollerClient/serialport.go create mode 100644 utility/directory.go create mode 100644 utility/paramsCheck.go create mode 100644 utility/timer/timed_task.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85e7c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2ec0e3d --- /dev/null +++ b/go.mod @@ -0,0 +1,81 @@ +module tgk-touch + +go 1.26.2 + +require ( + github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 + github.com/alibabacloud-go/dysmsapi-20170525/v5 v5.5.1 + github.com/alibabacloud-go/tea v1.4.0 + github.com/alibabacloud-go/tea-utils/v2 v2.0.9 + github.com/gogf/gf/v2 v2.10.0 + github.com/robfig/cron/v3 v3.0.1 + github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 + github.com/towgo/towgo v1.1.5 + go.bug.st/serial v1.6.4 + go.uber.org/zap v1.27.1 + gorm.io/gorm v1.30.0 + xorm.io/xorm v1.3.9 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect + github.com/alibabacloud-go/debug v1.0.1 // indirect + github.com/aliyun/credentials-go v1.4.5 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/creack/goselect v0.1.2 // indirect + github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-sql-driver/mysql v1.9.2 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect + github.com/golang-sql/sqlexp v0.1.0 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grokify/html-strip-tags-go v0.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-sqlite3 v1.14.28 // indirect + github.com/microsoft/go-mssqldb v1.6.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/olekukonko/errors v1.1.0 // indirect + github.com/olekukonko/ll v0.0.9 // indirect + github.com/olekukonko/tablewriter v1.1.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/crypto v0.38.0 // indirect + golang.org/x/net v0.40.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.25.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.5.7 // indirect + gorm.io/driver/postgres v1.6.0 // indirect + gorm.io/driver/sqlite v1.5.7 // indirect + gorm.io/driver/sqlserver v1.6.0 // indirect + xorm.io/builder v0.3.13 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9d2042d --- /dev/null +++ b/go.sum @@ -0,0 +1,499 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1 h1:/iHxaJhsFr0+xVFfbMr5vxz848jyiWuIEDhYq3y5odY= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0 h1:vcYCAze6p19qBW7MhZybIsqD8sMV8js0NyQM8JDnVtg= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0/go.mod h1:OQeznEEkTZ9OrhHJoDD8ZDq51FHgXjqtP9z6bEwBq9U= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= +github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0 h1:HCc0+LpPfpCKs6LGGLAhwBARt9632unrVcI6i8s/8os= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= +github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA= +github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8= +github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g= +github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY= +github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE= +github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8= +github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc= +github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.14/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 h1:LHhjxZkNWAKWepxcWyzgFgo0X6TUVhL7sC7ANc60p8A= +github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg= +github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ= +github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo= +github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA= +github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY= +github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= +github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc= +github.com/alibabacloud-go/dysmsapi-20170525/v5 v5.5.1 h1:CyJ1adk5jlg7acrbG1sgdZ+EXTZNZHwhNQAf6VFfySo= +github.com/alibabacloud-go/dysmsapi-20170525/v5 v5.5.1/go.mod h1:J1zab9/VxVJGdZ5pSK/BbUot7CkaSkRXdaLKAXXRLoY= +github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q= +github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE= +github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY= +github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws= +github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg= +github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4= +github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A= +github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk= +github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= +github.com/alibabacloud-go/tea v1.4.0 h1:MSKhu/kWLPX7mplWMngki8nNt+CyUZ+kfkzaR5VpMhA= +github.com/alibabacloud-go/tea v1.4.0/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg= +github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I= +github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE= +github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4= +github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/alibabacloud-go/tea-utils/v2 v2.0.9 h1:y6pUIlhjxbZl9ObDAcmA1H3c21eaAxADHTDQmBnAIgA= +github.com/alibabacloud-go/tea-utils/v2 v2.0.9/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I= +github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= +github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0= +github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM= +github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk= +github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= +github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= +github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= +github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs= +github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= +github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= +github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microsoft/go-mssqldb v0.19.0/go.mod h1:ukJCBnnzLzpVF0qYRT+eg1e+eSwjeQ7IvenUv8QPook= +github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc= +github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8= +github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= +github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= +github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= +github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY= +github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I= +github.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +github.com/towgo/towgo v1.1.5 h1:WkQapUEMIvipfDVP3uQXRPddi0pEdNmVfumXszWRRt4= +github.com/towgo/towgo v1.1.5/go.mod h1:nW4bjLhlr0fapP5Cv7QsqnkiuaOsXt1sM7EwkuPX+w0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo= +gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc= +gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= +modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk= +modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE= +modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= +modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= +xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= +xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU= +xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw= diff --git a/internal/core/internal/constant.go b/internal/core/internal/constant.go new file mode 100644 index 0000000..3b1b41f --- /dev/null +++ b/internal/core/internal/constant.go @@ -0,0 +1,78 @@ +package internal + +import ( + "github.com/gogf/gf/v2/util/gconv" + "github.com/towgo/towgo/errors/terror" + "regexp" + "strings" + "time" +) + +// 单位与字节的换算关系(1单位 = 多少字节) +var unitMap = map[string]int64{ + "b": 1, // 字节 + "kb": 1 << 10, // 1KB = 1024B + "mb": 1 << 20, // 1MB = 1024KB + "gb": 1 << 30, // 1GB = 1024MB + "tb": 1 << 40, // 1TB = 1024GB +} + +// 单位与秒的换算(1单位 = 多少秒) +var durationMap = map[string]int64{ + "s": 1, + "m": 60, + "h": 3600, + "d": 86400, +} + +// parseSize 将带单位的大小字符串(如"100kb"、"2MB")转换为字节数 +func parseSize(sizeStr string) (int64, error) { + if sizeStr == "" { + return 0, terror.New("空的大小配置") + } + + // 正则匹配:提取数字和单位(支持整数/小数,如"1.5mb") + re := regexp.MustCompile(`^(\d+(\.\d+)?)([a-zA-Zb]+)$`) + matches := re.FindStringSubmatch(strings.TrimSpace(sizeStr)) + if len(matches) != 4 { + return 0, terror.New("无效的大小格式(示例:100kb、2mb、1g)") + } + + // 提取数值和单位 + valueStr := matches[1] + unit := strings.ToLower(matches[3]) // 单位转为小写(统一处理) + + // 转换数值为float64 + + value := gconv.Float64(valueStr) + // 查找单位对应的字节数 + unitBytes, ok := unitMap[unit] + if !ok { + return 0, terror.Newf("不支持的单位:{%+v}(支持:b、kb、mb、gb、tb)", unit) + } + + // 计算总字节数(数值 * 单位字节数) + return int64(value * float64(unitBytes)), nil +} + +// parseDuration 将带单位的时间字符串(如"30s"、"5m")转换为time.Duration +func parseDuration(intervalStr string) (time.Duration, error) { + if intervalStr == "" { + return 30 * time.Second, nil // 默认30秒 + } + + re := regexp.MustCompile(`^(\d+)([smhd])$`) // 匹配数字+单位(s/m/h/d) + matches := re.FindStringSubmatch(strings.TrimSpace(intervalStr)) + if len(matches) != 3 { + return 0, terror.New("无效的时间格式(示例:30s、5m、1h)") + } + + num := gconv.Int(matches[1]) + unit := matches[2] + sec, ok := durationMap[unit] + if !ok { + return 0, terror.New("不支持的单位(仅支持s/m/h/d)") + } + + return time.Duration(num) * time.Duration(sec) * time.Second, nil +} diff --git a/internal/core/internal/cutter.go b/internal/core/internal/cutter.go new file mode 100644 index 0000000..a5da074 --- /dev/null +++ b/internal/core/internal/cutter.go @@ -0,0 +1,392 @@ +package internal + +import ( + "compress/gzip" + "context" + "fmt" + "github.com/gogf/gf/v2/util/gconv" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "sync" + "time" +) + +// Cutter 实现 io.Writer 接口 +// 用于日志切割, strings.Join([]string{director,layout, formats..., level+".log"}, os.PathSeparator) +type Cutter struct { + level string // 日志级别(debug, info, warn, error, dpanic, panic, fatal) + layout string // 时间格式 2006-01-02 15:04:05 + formats []string // 自定义参数([]string{Director,time.DateOnly, "business"(此参数可不写), level+".log"} + director string // 日志文件夹 + retentionDay int // 日志保留天数 + // 新增以下字段 + maxSize int64 // 单文件最大大小(字节) + compress bool // 是否压缩旧文件 + currentSize int64 // 当前文件已写入大小 + fileIndex int // 同级别下的文件序号(用于大小切割) + lastLayout string // 上一次的时间字符串(判断是否跨天) + file *os.File // 文件句柄 + mutex *sync.RWMutex // 读写锁 + compressLevel int + // 新增:定时任务相关 + checkInterval time.Duration // 检查间隔(转换后的时间) + ticker *time.Ticker // 定时触发器 + ctx context.Context + cancel context.CancelFunc // 用于停止定时任务 + +} + +type CutterOption func(*Cutter) + +// 新增选项:设置单文件最大大小(MB转换为字节) +func CutterWithMaxSize(sizeStr string) CutterOption { + return func(c *Cutter) { + if sizeStr == "" { + c.maxSize = 0 // 不限制大小 + return + } + // 解析字符串为字节数 + bytes, err := parseSize(sizeStr) + if err != nil { + // 解析失败时,默认不限制大小,并打印警告 + fmt.Printf("日志大小配置解析失败: %v,将不限制文件大小\n", err) + c.maxSize = 0 + return + } + c.maxSize = bytes + } +} + +// 新增选项:设置定时检查间隔 +func CutterWithCheckInterval(intervalStr string) CutterOption { + return func(c *Cutter) { + interval, err := parseDuration(intervalStr) + if err != nil { + fmt.Printf("时间间隔解析失败: %v,使用默认30秒\n", err) + interval = 30 * time.Second + } + c.checkInterval = interval + } +} + +// 初始化Cutter时启动定时任务 +func NewCutter(director string, level string, retentionDay int, options ...CutterOption) *Cutter { + ctx, cancel := context.WithCancel(context.Background()) + rotate := &Cutter{ + level: level, + director: director, + retentionDay: retentionDay, + mutex: new(sync.RWMutex), + ctx: ctx, + cancel: cancel, + checkInterval: 30 * time.Second, // 默认30秒 + } + for _, opt := range options { + opt(rotate) + } + + // 启动定时检查任务 + rotate.startTicker() + + // 关键:启动时扫描历史文件,获取最大序号,设置初始fileIndex + rotate.lastLayout = time.Now().Format(rotate.layout) // 先初始化当前时间布局 + rotate.fileIndex = rotate.getMaxFileIndex() // 从最大序号开始 + return rotate +} + +// 启动定时检查的ticker +func (c *Cutter) startTicker() { + c.ticker = time.NewTicker(c.checkInterval) + go func() { + for { + select { + case <-c.ctx.Done(): + c.ticker.Stop() + return + case <-c.ticker.C: + + c.checkAndRotate() // 定时执行检查和切割 + + } + } + }() +} + +// 定时检查是否需要切割和压缩 +func (c *Cutter) checkAndRotate() { + c.mutex.Lock() + defer c.mutex.Unlock() + // 定时删除过期文件 + defer removeNDaysFolders(c.director, c.retentionDay) + + // 1. 检查是否跨天(时间切割) + currentLayout := time.Now().Format(c.layout) + if currentLayout != c.lastLayout { + c.rotateFile(currentLayout) // 跨天切割 + return + } + + // 2. 检查文件大小是否超限(大小切割) + if c.maxSize > 0 && c.currentSize > c.maxSize { + c.rotateFile(currentLayout) // 大小超限切割 + } +} + +// 执行切割逻辑(创建新文件,压缩旧文件) +func (c *Cutter) rotateFile(currentLayout string) { + // 关闭当前文件(如果存在) + if c.file != nil { + _ = c.file.Close() + c.file = nil + } + + // 压缩旧文件(如果开启压缩) + oldFilename := c.getFilename(c.lastLayout, c.fileIndex) + + if c.currentSize > 0 && c.compress { // 避免压缩刚创建的文件 + // 异步压缩,不阻塞定时任务 + go c.compressFile(oldFilename) + } + + // 更新状态,准备新文件 + // 更新状态:跨天切割重置序号,大小切割累加序号 + if currentLayout != c.lastLayout { + // 跨天:序号从0开始 + c.fileIndex = 0 + } else { + // 大小超限:序号+1 + c.fileIndex++ + } + c.lastLayout = currentLayout + if c.currentSize == 0 { + return + } + c.currentSize = 0 + // 预创建新文件(可选,避免首次写入时创建的开销) + newFilename := c.getFilename(currentLayout, c.fileIndex) + if err := os.MkdirAll(filepath.Dir(newFilename), os.ModePerm); err != nil { + fmt.Printf("创建新文件目录失败: %v\n", err) + return + } + // 打开新文件(追加模式,确保文件存在) + if f, err := os.OpenFile(newFilename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644); err == nil { + c.file = f + } else { + fmt.Printf("预创建新文件失败: %v\n", err) + } +} + +// 新增选项:设置是否压缩旧文件 +func CutterWithCompress(compress bool) CutterOption { + return func(c *Cutter) { + c.compress = compress + } +} + +// CutterWithLayout 时间格式 +func CutterWithLayout(layout string) CutterOption { + return func(c *Cutter) { + c.layout = layout + } +} + +// CutterWithFormats 格式化参数 +func CutterWithFormats(format ...string) CutterOption { + return func(c *Cutter) { + if len(format) > 0 { + c.formats = format + } + } +} + +// 新增选项:设置压缩等级 +func CutterWithCompressLevel(level int) CutterOption { + return func(c *Cutter) { + // 验证等级有效性(1-9),无效则用默认值6 + if level < 1 || level > 9 { + //fmt.Printf("无效的压缩等级 %d(范围1-9),使用默认等级6\n", level) + c.compressLevel = 6 + return + } + c.compressLevel = level + } +} + +// 辅助函数:扫描当前日期目录下的日志文件,返回最大序号 +func (c *Cutter) getMaxFileIndex() int { + // 1. 构建当前日期的目录路径(与日志写入的目录一致) + currentLayout := time.Now().Format(c.layout) + dirParts := []string{c.director} + if currentLayout != "" { + dirParts = append(dirParts, currentLayout) + } + dirParts = append(dirParts, c.formats...) + currentDir := filepath.Join(dirParts...) + + // 2. 匹配文件名格式:级别.序号.log 或 级别.序号.log.gz(如 info.3.log、error.2.log.gz) + // 正则表达式:匹配 "级别.数字.log" 或 "级别.数字.log.gz" + pattern := filepath.Join(currentDir, fmt.Sprintf("%s.*.log", c.level)) + files, err := filepath.Glob(pattern) + if err != nil || len(files) == 0 { + return 0 // 目录不存在或无文件,从0开始 + } + maxIndex := 0 + isMaxFileGz := false // 标记最大序号文件是否为.gz + re := regexp.MustCompile(fmt.Sprintf(`%s.(\d+).log`, c.level)) + + for _, file := range files { + filename := filepath.Base(file) + matches := re.FindStringSubmatch(filename) + if len(matches) < 2 { + continue + } + + index := gconv.Int(matches[1]) + + // 关键:更新最大序号时,同步记录该文件是否为.gz + if index > maxIndex { + maxIndex = index + // 判断文件是否以.gz结尾(注意:用完整路径判断,避免文件名包含.gz但不是后缀) + isMaxFileGz = strings.HasSuffix(file, ".gz") + } else if index == maxIndex { + // 极端情况:同一序号存在.log和.log.gz(如info.2.log和info.2.log.gz) + // 优先以「非.gz」为准(说明是最新的未压缩文件) + if !strings.HasSuffix(file, ".gz") { + isMaxFileGz = false + } + } + } + if isMaxFileGz { + return maxIndex + 1 + } + return maxIndex +} + +// Write satisfies the io.Writer interface. It writes to the +// appropriate file handle that is currently being used. +// If we have reached rotation time, the target file gets +// automatically rotated, and also purged if necessary. +func (c *Cutter) Write(bytes []byte) (n int, err error) { + c.mutex.Lock() + defer c.mutex.Unlock() + + // 确保文件句柄有效(如果未打开则创建) + + // 新增:跨天轻量检查(仅在写入时判断,发现跨天立即切割) + currentLayout := time.Now().Format(c.layout) + if currentLayout != c.lastLayout { + // 调用切割逻辑,切换到新日期目录 + // 切割后重新生成新文件名(此时c.lastLayout已更新) + c.rotateFile(currentLayout) + } + filename := c.getFilename(currentLayout, c.fileIndex) + if c.file == nil { + if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil { + return 0, err + } + f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return 0, err + } + c.file = f + // 初始化文件大小(如果是新打开的文件) + if c.currentSize == 0 { + if fi, err := os.Stat(filename); err == nil { + c.currentSize = fi.Size() + } + } + } + + // 写入日志并更新大小 + n, err = c.file.Write(bytes) + if err == nil { + c.currentSize += int64(n) + } + return n, err +} + +// 辅助函数:生成带序号的文件名 +func (c *Cutter) getFilename(layout string, index int) string { + values := []string{c.director} + if layout != "" { + values = append(values, layout) + } + values = append(values, c.formats...) + // 文件名格式:级别.序号.log(如 error.0.log) + values = append(values, fmt.Sprintf("%s.%d.log", c.level, index)) + return filepath.Join(values...) +} + +func (c *Cutter) Sync() error { + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.file != nil { + return c.file.Sync() + } + return nil +} + +// 压缩文件为.gz(应用指定压缩等级) +func (c *Cutter) compressFile(src string) error { // 改为成员方法,方便使用c.compressLevel + + if _, err := os.Stat(src); os.IsNotExist(err) { + return nil // 原文件不存在,无需压缩 + } + // 打开原文件 + srcFile, err := os.Open(src) + if err != nil { + return fmt.Errorf("打开原文件失败: %w", err) + } + defer srcFile.Close() + + // 创建压缩文件(如 info.0.log.gz) + dst := src + ".gz" + dstFile, err := os.Create(dst) + if err != nil { + return fmt.Errorf("创建压缩文件失败: %w", err) + } + defer dstFile.Close() + + // 关键:使用指定压缩等级创建gzip写入器 + gzWriter, err := gzip.NewWriterLevel(dstFile, c.compressLevel) + if err != nil { + return fmt.Errorf("初始化gzip写入器失败: %w", err) + } + defer gzWriter.Close() + + // 执行压缩(将原文件内容写入gzip) + if _, err := io.Copy(gzWriter, srcFile); err != nil { + return fmt.Errorf("压缩数据失败: %w", err) + } + + // 压缩成功后删除原文件 + if err := os.Remove(src); err != nil { + return fmt.Errorf("删除原文件失败: %w", err) + } + + return nil +} + +// 增加日志目录文件清理 小于等于零的值默认忽略不再处理 +func removeNDaysFolders(dir string, days int) error { + if days <= 0 { + return nil + } + cutoff := time.Now().AddDate(0, 0, -days) + return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.ModTime().Before(cutoff) && path != dir { + err = os.RemoveAll(path) + if err != nil { + return err + } + } + return nil + }) +} diff --git a/internal/core/internal/zap_core.go b/internal/core/internal/zap_core.go new file mode 100644 index 0000000..f7b11ac --- /dev/null +++ b/internal/core/internal/zap_core.go @@ -0,0 +1,74 @@ +package internal + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "os" + "tgk-touch/internal/global" + "time" +) + +type ZapCore struct { + level zapcore.Level + zapcore.Core +} + +func NewZapCore(level zapcore.Level) *ZapCore { + entity := &ZapCore{level: level} + syncer := entity.WriteSyncer() + levelEnabler := zap.LevelEnablerFunc(func(l zapcore.Level) bool { + return l == level + }) + entity.Core = zapcore.NewCore(g.GVA_Config.Zap.Encoder(), syncer, levelEnabler) + return entity +} + +func (z *ZapCore) WriteSyncer(formats ...string) zapcore.WriteSyncer { + + cutter := NewCutter( + g.GVA_Config.Zap.Director, + z.level.String(), + g.GVA_Config.Zap.RetentionDay, + CutterWithLayout(time.DateOnly), + CutterWithFormats(formats...), + CutterWithMaxSize(g.GVA_Config.Zap.MaxSize), // 传入最大大小(MB) + CutterWithCompress(g.GVA_Config.Zap.Compress), // 传入是否压缩 + CutterWithCompressLevel(g.GVA_Config.Zap.CompressLevel), + CutterWithCheckInterval(g.GVA_Config.Zap.CheckInterval), + ) + if g.GVA_Config.Zap.LogInConsole { + multiSyncer := zapcore.NewMultiWriteSyncer(os.Stdout, cutter) + return zapcore.AddSync(multiSyncer) + } + return zapcore.AddSync(cutter) +} + +func (z *ZapCore) Enabled(level zapcore.Level) bool { + return z.level == level +} + +func (z *ZapCore) With(fields []zapcore.Field) zapcore.Core { + return z.Core.With(fields) +} + +func (z *ZapCore) Check(entry zapcore.Entry, check *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if z.Enabled(entry.Level) { + return check.AddCore(entry, z) + } + return check +} + +func (z *ZapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error { + for i := 0; i < len(fields); i++ { + if fields[i].Key == "business" || fields[i].Key == "folder" || fields[i].Key == "directory" { + syncer := z.WriteSyncer(fields[i].String) + z.Core = zapcore.NewCore(g.GVA_Config.Zap.Encoder(), syncer, z.level) + } + } + + return z.Core.Write(entry, fields) +} + +func (z *ZapCore) Sync() error { + return z.Core.Sync() +} diff --git a/internal/core/server_run.go b/internal/core/server_run.go new file mode 100644 index 0000000..246d6dd --- /dev/null +++ b/internal/core/server_run.go @@ -0,0 +1,71 @@ +package core + +import ( + "context" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "tgk-touch/internal/global" + "time" + + "go.uber.org/zap" +) + +type server interface { + ListenAndServe() error + Shutdown(context.Context) error +} + +func RunServer() { + + initServer( + fmt.Sprintf(":%d", g.GVA_Config.Server.Port), + g.GVA_HttpServeMux, + 10*time.Minute, + 10*time.Minute, + ) +} + +// initServer 启动服务并实现优雅关闭 +func initServer(address string, router *http.ServeMux, readTimeout, writeTimeout time.Duration) { + // 创建服务 + srv := &http.Server{ + Addr: address, + Handler: router, + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + MaxHeaderBytes: 1 << 20, + } + + // 在goroutine中启动服务 + go func() { + zap.S().Info("Start server at " + address) + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("listen: %s\n", err) + zap.L().Error("server启动失败", zap.Error(err)) + os.Exit(1) + } + }() + + // 等待中断信号以优雅地关闭服务器 + quit := make(chan os.Signal, 1) + // kill (无参数) 默认发送 syscall.SIGTERM + // kill -2 发送 syscall.SIGINT + // kill -9 发送 syscall.SIGKILL,但是无法被捕获,所以不需要添加 + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + zap.S().Info("关闭WEB服务...") + + // 设置5秒的超时时间 + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + + defer cancel() + + if err := srv.Shutdown(ctx); err != nil { + zap.L().Fatal("WEB服务关闭异常", zap.Error(err)) + } + + zap.S().Info("WEB服务已关闭") +} diff --git a/internal/core/zap.go b/internal/core/zap.go new file mode 100644 index 0000000..89e5bfe --- /dev/null +++ b/internal/core/zap.go @@ -0,0 +1,38 @@ +package core + +import ( + "fmt" + "os" + "tgk-touch/internal/core/internal" + "tgk-touch/internal/global" + "tgk-touch/utility" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +// Zap 获取 zap.Logger +// Author [SliverHorn](https://github.com/SliverHorn) +func Zap() (logger *zap.Logger) { + z := g.GVA_Config.Zap + if ok, _ := utility.PathExists(z.Director); !ok { // 判断是否有Director文件夹 + fmt.Printf("create %v directory\n", z.Director) + _ = os.Mkdir(z.Director, os.ModePerm) + } + levels := z.Levels() + length := len(levels) + cores := make([]zapcore.Core, 0, length) + for i := 0; i < length; i++ { + core := internal.NewZapCore(levels[i]) + + cores = append(cores, core) + } + logger = zap.New( + zapcore.NewTee(cores...), + zap.AddStacktrace(zapcore.ErrorLevel)) + + if z.ShowLine { + logger = logger.WithOptions(zap.AddCaller()) + } + return logger +} diff --git a/internal/global/global.go b/internal/global/global.go new file mode 100644 index 0000000..44cdbee --- /dev/null +++ b/internal/global/global.go @@ -0,0 +1,47 @@ +package g + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gcfg" + "go.uber.org/zap" + "net/http" + "tgk-touch/internal/model/config" + "tgk-touch/utility/timer" +) + +var ( + GVA_Timer timer.Timer = timer.NewTimerTask() + GVA_HttpServeMux = http.NewServeMux() + GVA_Config config.AppConfig + GVA_LOG *zap.Logger + GVA_HttpServer *http.Server +) + +func HttpServerMux() *http.ServeMux { + return GVA_HttpServeMux +} +func HttpServer() *http.Server { + return GVA_HttpServer +} + +func Timer() timer.Timer { + return GVA_Timer +} +func Config() config.AppConfig { + return GVA_Config +} +func Cfg() *gcfg.Config { + return g.Cfg() +} +func Log() *zap.SugaredLogger { + if GVA_LOG == nil { + return zap.S() + } + return GVA_LOG.Sugar() +} +func JsonLog() *zap.Logger { + if GVA_LOG == nil { + return zap.L() + } + return GVA_LOG +} diff --git a/internal/global/model.go b/internal/global/model.go new file mode 100644 index 0000000..dd49455 --- /dev/null +++ b/internal/global/model.go @@ -0,0 +1,13 @@ +package g + +type PageResult struct { + Rows any `json:"rows"` + Count int64 `json:"count"` +} + +func NewPageResult(c int64, r any) *PageResult { + return &PageResult{ + Rows: r, + Count: c, + } +} diff --git a/internal/initialize/init.go b/internal/initialize/init.go new file mode 100644 index 0000000..25fc419 --- /dev/null +++ b/internal/initialize/init.go @@ -0,0 +1,43 @@ +package initialize + +import ( + "github.com/gogf/gf/v2/os/gcfg" + "github.com/gogf/gf/v2/os/gctx" + "go.uber.org/zap" + "tgk-touch/internal/core" + g "tgk-touch/internal/global" + "tgk-touch/internal/model/config" + "tgk-touch/internal/module/adl400TtyApi" + m9zTtyApi "tgk-touch/internal/module/m9zTtyApi" + "tgk-touch/internal/module/m9zTtyPwd" +) + +func Init() { + CfgInit() + ZapInit() + m9zTtyPwd.Init() + m9zTtyApi.Init() + adl400TtyApi.Init() + timer() + +} + +func CfgInit() { + + // 读取配置文件 + loader := gcfg.NewLoader[config.AppConfig](g.Cfg(), "") + // 设置配置变更回调 + loader.OnChange(func(updated config.AppConfig) error { + g.GVA_Config = updated + ZapInit() + return nil + }) + loader.MustLoadAndWatch(gctx.New(), "app-config") + g.GVA_Config = loader.Get() + +} +func ZapInit() { + // 初始化zap日志库 + g.GVA_LOG = core.Zap() + zap.ReplaceGlobals(g.GVA_LOG) +} diff --git a/internal/initialize/timer.go b/internal/initialize/timer.go new file mode 100644 index 0000000..20d98a7 --- /dev/null +++ b/internal/initialize/timer.go @@ -0,0 +1,49 @@ +package initialize + +import ( + "github.com/robfig/cron/v3" + "github.com/towgo/towgo/errors/tcode" + "github.com/towgo/towgo/errors/terror" + "sync" + g "tgk-touch/internal/global" + m9zTtyApi "tgk-touch/internal/module/m9zTtyApi" +) + +func timer() { + go func() { + AddTaskByFunc("ScanM9z", "ScanM9z", "@every 10m", m9zTtyApi.ScanM9z) + + }() +} + +var cronTaskNameMap = map[string]cron.EntryID{} +var cronLock = sync.Mutex{} + +func AddTaskByFunc(cronName string, taskName string, spec string, task func()) { + cronLock.Lock() + defer cronLock.Unlock() + cronTaskName := cronName + "-" + taskName + _, ok := cronTaskNameMap[cronTaskName] + if ok { + panic(terror.New("定时任务名称重复 " + cronTaskName)) + } + var option []cron.Option + option = append(option, cron.WithSeconds(), cron.WithChain(cron.SkipIfStillRunning(cron.DefaultLogger))) + cronEnterId, err := g.Timer().AddTaskByFunc(cronName, spec, func() { + defer func() { + if exception := recover(); exception != nil { + if errv, ok := exception.(error); ok { + g.GVA_LOG.Sugar().Errorf("corn recovered panic err task [%s] sepc [%s] err:%+v \n", taskName, spec, terror.NewCodef(tcode.CodeInternalPanic, "%+v", errv)) + } + } + }() + + task() + + }, taskName, option...) + if err != nil { + g.Log().Panicf("add timer error: [%s]", err.Error()) + } + cronTaskNameMap[cronTaskName] = cronEnterId + g.GVA_LOG.Sugar().Infof("add timer success: cronName [%s] task [%s] sepc [%s]", cronName, taskName, spec) +} diff --git a/internal/library/ADL200/cmd.go b/internal/library/ADL200/cmd.go new file mode 100644 index 0000000..4a7ec2e --- /dev/null +++ b/internal/library/ADL200/cmd.go @@ -0,0 +1,156 @@ +package ADL200 + +import "fmt" + +// ADL200 单相电表 Modbus 寄存器地址定义 +// 参考《ADL200单相电子式电能表使用说明书 V1.2》 +var ( + // ===== 电能数据 ===== + H0000 = MeterModusCmd{startAddr: 0x0000, quantity: 2, detail: "组合有功总电能"} // 4字节, 单位0.01kWh + H0002 = MeterModusCmd{startAddr: 0x0002, quantity: 2, detail: "组合有功尖电能"} // 4字节 + H0004 = MeterModusCmd{startAddr: 0x0004, quantity: 2, detail: "组合有功峰电能"} // 4字节 + H0006 = MeterModusCmd{startAddr: 0x0006, quantity: 2, detail: "组合有功平电能"} // 4字节 + H0008 = MeterModusCmd{startAddr: 0x0008, quantity: 2, detail: "组合有功谷电能"} // 4字节 + H000A = MeterModusCmd{startAddr: 0x000A, quantity: 1, detail: "密码"} // 2字节 + + // ===== 实时数据 ===== + H000B = MeterModusCmd{startAddr: 0x000B, quantity: 1, detail: "电压"} // 单位: 0.1V + H000C = MeterModusCmd{startAddr: 0x000C, quantity: 1, detail: "电流"} // 单位: 0.01A + H000D = MeterModusCmd{startAddr: 0x000D, quantity: 1, detail: "有功功率"} // 单位: 0.001kW + H000E = MeterModusCmd{startAddr: 0x000E, quantity: 1, detail: "无功功率"} // 单位: 0.001kvar + H000F = MeterModusCmd{startAddr: 0x000F, quantity: 1, detail: "视在功率"} // 单位: 0.001kVA + H0010 = MeterModusCmd{startAddr: 0x0010, quantity: 1, detail: "功率因数"} // 单位: 0.001 + H0011 = MeterModusCmd{startAddr: 0x0011, quantity: 1, detail: "频率"} // 单位: 0.01Hz + + // ===== 时间设置 ===== + H0012 = MeterModusCmd{startAddr: 0x0012, quantity: 1, cmdType: 1, detail: "年月"} // R/W + H0013 = MeterModusCmd{startAddr: 0x0013, quantity: 1, cmdType: 1, detail: "日时"} // R/W + H0014 = MeterModusCmd{startAddr: 0x0014, quantity: 1, cmdType: 1, detail: "分秒"} // R/W + + // ===== 通信设置 ===== + H0015 = MeterModusCmd{startAddr: 0x0015, quantity: 1, cmdType: 1, detail: "高8位地址/低8位波特率"} // R/W + H0016 = MeterModusCmd{startAddr: 0x0016, quantity: 1, cmdType: 1, detail: "背光时间"} // R/W + + // ===== 历史电能 (上月) ===== + H0022 = MeterModusCmd{startAddr: 0x0022, quantity: 2, detail: "上1月组合有功总电能"} // 4字节 + H0024 = MeterModusCmd{startAddr: 0x0024, quantity: 2, detail: "上1月组合有功尖电能"} + H0026 = MeterModusCmd{startAddr: 0x0026, quantity: 2, detail: "上1月组合有功峰电能"} + H0028 = MeterModusCmd{startAddr: 0x0028, quantity: 2, detail: "上1月组合有功平电能"} + H002A = MeterModusCmd{startAddr: 0x002A, quantity: 2, detail: "上1月组合有功谷电能"} + H002C = MeterModusCmd{startAddr: 0x002C, quantity: 2, detail: "上2月组合有功总电能"} + H002E = MeterModusCmd{startAddr: 0x002E, quantity: 2, detail: "上2月组合有功尖电能"} + H0030 = MeterModusCmd{startAddr: 0x0030, quantity: 2, detail: "上2月组合有功峰电能"} + H0032 = MeterModusCmd{startAddr: 0x0032, quantity: 2, detail: "上2月组合有功平电能"} + H0034 = MeterModusCmd{startAddr: 0x0034, quantity: 2, detail: "上2月组合有功谷电能"} + H0036 = MeterModusCmd{startAddr: 0x0036, quantity: 2, detail: "上3月组合有功总电能"} + H0038 = MeterModusCmd{startAddr: 0x0038, quantity: 2, detail: "上3月组合有功尖电能"} + H003A = MeterModusCmd{startAddr: 0x003A, quantity: 2, detail: "上3月组合有功峰电能"} + H003C = MeterModusCmd{startAddr: 0x003C, quantity: 2, detail: "上3月组合有功平电能"} + H003E = MeterModusCmd{startAddr: 0x003E, quantity: 2, detail: "上3月组合有功谷电能"} + + // ===== 状态和配置 ===== + H0045 = MeterModusCmd{startAddr: 0x0045, quantity: 1, cmdType: 1, detail: "状态"} + // Bit0: 费率类型 0-非复费率, 1-复费率 + // Bit1: 接线方向 0-上进下出, 1-下进上出 + // Bit3: 视在功率 0-PQS, 1-RMS + H0048 = MeterModusCmd{startAddr: 0x0048, quantity: 1, detail: "校验方式"} // 0000=无校验, 0002=偶校验 + + // ===== 表号 ===== + H0049 = MeterModusCmd{startAddr: 0x0049, quantity: 1, cmdType: 1, detail: "表号低"} + H004A = MeterModusCmd{startAddr: 0x004A, quantity: 1, cmdType: 1, detail: "表号中"} + H004B = MeterModusCmd{startAddr: 0x004B, quantity: 1, cmdType: 1, detail: "表号高"} + + // ===== 正向有功电能 ===== + H0068 = MeterModusCmd{startAddr: 0x0068, quantity: 2, detail: "正向有功总电能"} // 4字节, 单位0.01kWh + H006A = MeterModusCmd{startAddr: 0x006A, quantity: 2, detail: "正向有功尖电能"} + H006C = MeterModusCmd{startAddr: 0x006C, quantity: 2, detail: "正向有功峰电能"} + H006E = MeterModusCmd{startAddr: 0x006E, quantity: 2, detail: "正向有功平电能"} + H0070 = MeterModusCmd{startAddr: 0x0070, quantity: 2, detail: "正向有功谷电能"} + + // ===== 反向有功��能 ===== + H0072 = MeterModusCmd{startAddr: 0x0072, quantity: 2, detail: "反向有功总电能"} + H0074 = MeterModusCmd{startAddr: 0x0074, quantity: 2, detail: "反向有功尖电能"} + H0076 = MeterModusCmd{startAddr: 0x0076, quantity: 2, detail: "反向有功峰电能"} + H0078 = MeterModusCmd{startAddr: 0x0078, quantity: 2, detail: "反向有功平电能"} + H007A = MeterModusCmd{startAddr: 0x007A, quantity: 2, detail: "反向有功谷电能"} + + // ===== 无功电能 ===== + H00B0 = MeterModusCmd{startAddr: 0x00B0, quantity: 2, detail: "当前总无功电能"} // 4字节, 单位0.01kVarh + H00B2 = MeterModusCmd{startAddr: 0x00B2, quantity: 2, detail: "当前无功尖电能"} + H00B4 = MeterModusCmd{startAddr: 0x00B4, quantity: 2, detail: "当前无功峰电能"} + H00B6 = MeterModusCmd{startAddr: 0x00B6, quantity: 2, detail: "当前无功平电能"} + H00B8 = MeterModusCmd{startAddr: 0x00B8, quantity: 2, detail: "当前无功谷电能"} + H00BA = MeterModusCmd{startAddr: 0x00BA, quantity: 2, detail: "当前正向总无功电能"} + H00BC = MeterModusCmd{startAddr: 0x00BC, quantity: 2, detail: "当前正向无功尖电能"} + H00BE = MeterModusCmd{startAddr: 0x00BE, quantity: 2, detail: "当前正向无功峰电能"} + H00C0 = MeterModusCmd{startAddr: 0x00C0, quantity: 2, detail: "当前正向无功平电能"} + H00C2 = MeterModusCmd{startAddr: 0x00C2, quantity: 2, detail: "当前正向无功谷电能"} + H00C4 = MeterModusCmd{startAddr: 0x00C4, quantity: 2, detail: "当前反向总无功电能"} + H00C6 = MeterModusCmd{startAddr: 0x00C6, quantity: 2, detail: "当前反向无功尖电能"} + H00C8 = MeterModusCmd{startAddr: 0x00C8, quantity: 2, detail: "当前反向无功峰电能"} + H00CA = MeterModusCmd{startAddr: 0x00CA, quantity: 2, detail: "当前反向无功平电能"} + H00CC = MeterModusCmd{startAddr: 0x00CC, quantity: 2, detail: "当前反向无功谷电能"} +) + +// MeterModusCmd Modbus 寄存器命令结构体 +type MeterModusCmd struct { + startAddr uint16 // 起始地址 + quantity uint16 // 寄存器数量 + cmdType int // 0: 只读, 1: 可读写 + detail string // 描述信息 +} + +// SetStartAddr 设置起始地址 +func (mcc *MeterModusCmd) SetStartAddr(s uint16) { + mcc.startAddr = s +} + +// GetStartAddr 获取起始地址 +func (mcc *MeterModusCmd) GetStartAddr() uint16 { + return mcc.startAddr +} + +// SetQuantity 设置寄存器数量 +func (mcc *MeterModusCmd) SetQuantity(q uint16) { + mcc.quantity = q +} + +// GetQuantity 获取寄存器数量 +func (mcc *MeterModusCmd) GetQuantity() uint16 { + return mcc.quantity +} + +// SetCmdType 设置命令类型 +func (mcc *MeterModusCmd) SetCmdType(t int) { + mcc.cmdType = t +} + +// GetCmdType 获取命令类型 +func (mcc *MeterModusCmd) GetCmdType() int { + return mcc.cmdType +} + +// Read 读取寄存器数据 +func (mmc *MeterModusCmd) Read(commUid string, addr byte) ([]uint16, error) { + return readCmd(commUid, addr, mmc.startAddr, mmc.quantity, "读取"+mmc.detail) +} + +// Write 写入寄存器数据 +func (mmc *MeterModusCmd) Write(commUid string, addr byte, data []uint16) error { + if mmc.cmdType == 0 { + return nil + } + return writeCmd(commUid, addr, mmc.startAddr, data, "写入"+mmc.detail) +} + +// GetHistoricalMonthAddress 获取历史月份电能的起始地址 +// month: 1-3 (上1月到上3月) +func GetHistoricalMonthAddress(month int) (*MeterModusCmd, error) { + if month < 1 || month > 3 { + return nil, fmt.Errorf("invalid month value: %d (valid range 1-3)", month) + } + // 上1月: 0x0022, 上2月: 0x002C, 上3月: 0x0036 + address := 0x0022 + uint16((month-1)*10) + MMC := &MeterModusCmd{startAddr: address, quantity: 10, detail: fmt.Sprintf("上%d月组合有功电能", month)} + return MMC, nil +} diff --git a/internal/library/ADL200/cmdBuild.go b/internal/library/ADL200/cmdBuild.go new file mode 100644 index 0000000..3b593fe --- /dev/null +++ b/internal/library/ADL200/cmdBuild.go @@ -0,0 +1,147 @@ +package ADL200 + +import ( + "encoding/binary" + "fmt" + "github.com/sigurn/crc16" +) + +// ReadHoldingRegistersBuilder 构造读取保持寄存器命令 (功能码 0x03) +func ReadHoldingRegistersBuilder(address byte, startAddr, quantity uint16) []byte { + // 构造 Modbus-RTU 请求帧 + request := make([]byte, 6) + request[0] = address // 从站地址 + request[1] = 0x03 // 功能码: 读取保持寄存器 + binary.BigEndian.PutUint16(request[2:4], startAddr) // 起始地址 (大端) + binary.BigEndian.PutUint16(request[4:6], quantity) // 寄存器数量 (大端) + + // 计算 CRC16 Modbus 校验 + crcTable := crc16.MakeTable(crc16.CRC16_MODBUS) + crc := crc16.Checksum(request[:6], crcTable) + // 添加 CRC,低字节在前 + request = append(request, byte(crc&0xFF), byte(crc>>8)) + + return request +} + +// ReadHoldingRegisterParse 解析保持寄存器响应数据 +func ReadHoldingRegisterParse(result []byte, address byte, quantity uint16) ([]uint16, error) { + // 检查响应最小长度 + if len(result) < 5 { + return nil, fmt.Errorf("响应���度不足: %d bytes", len(result)) + } + + // 提取数据和 CRC + data := result[:len(result)-2] + receivedCRC := binary.LittleEndian.Uint16(result[len(result)-2:]) // 解析 CRC (小端) + + // 校验 CRC + crcTable := crc16.MakeTable(crc16.CRC16_MODBUS) + calculatedCRC := crc16.Checksum(data, crcTable) + if calculatedCRC != receivedCRC { + return nil, fmt.Errorf("CRC校验失败,计算值: %x,接收值: %x", calculatedCRC, receivedCRC) + } + + // 验证地址和功能码 + if data[0] != address { + return nil, fmt.Errorf("响应地址不符,期望: %x,实际: %x", address, data[0]) + } + + switch data[1] { + case 0x03: // 正常响应 + byteCount := data[2] + if len(data) < 3+int(byteCount) { + return nil, fmt.Errorf("响应数据不完整") + } + if int(byteCount) != 2*int(quantity) { + return nil, fmt.Errorf("数据长度不符,期望: %d,实际: %d", 2*quantity, byteCount) + } + + // 解析寄存器值 (大端序) + registers := make([]uint16, quantity) + for i := 0; i < int(quantity); i++ { + registers[i] = binary.BigEndian.Uint16(data[3+2*i : 5+2*i]) + } + return registers, nil + + case 0x83: // 异常响应 + if len(data) < 3 { + return nil, fmt.Errorf("异常响应格式错误") + } + return nil, fmt.Errorf("Modbus异常码: %x", data[2]) + + default: + return nil, fmt.Errorf("未知功能码: %x", data[1]) + } +} + +// WriteMultipleRegistersBuilder 构造写入多个保持寄存器命令 (功能码 0x10) +func WriteMultipleRegistersBuilder(address byte, startAddr uint16, data []uint16) (writeData []byte, quantity uint16) { + // 构造 Modbus-RTU 请求帧 + quantity = uint16(len(data)) + byteCount := 2 * quantity + + request := make([]byte, 7+byteCount) + request[0] = address // 从站地址 + request[1] = 0x10 // 功能码: 写多个寄存器 + binary.BigEndian.PutUint16(request[2:4], startAddr) // 起始地址 + binary.BigEndian.PutUint16(request[4:6], quantity) // 寄存器数量 + request[6] = byte(byteCount) // 字节数 + + // 填充数据 (大端序) + for i, value := range data { + binary.BigEndian.PutUint16(request[7+2*i:9+2*i], value) + } + + // 计算 CRC 校验 + crcTable := crc16.MakeTable(crc16.CRC16_MODBUS) + crc := crc16.Checksum(request[:7+byteCount], crcTable) + // 添加 CRC (低字节在前) + request = append(request, byte(crc&0xFF), byte(crc>>8)) + + return request, quantity +} + +// WriteMultipleRegistersParse 解析写入多个保持���存器响应 +func WriteMultipleRegistersParse(address byte, startAddr uint16, quantity uint16, response []byte) error { + // 检查响应最小长度 + if len(response) < 6 { + return fmt.Errorf("响应长度不足: %d bytes", len(response)) + } + + // 提取数据和 CRC + respData := response[:len(response)-2] + receivedCRC := binary.LittleEndian.Uint16(response[len(response)-2:]) + + // 校验 CRC + crcTable := crc16.MakeTable(crc16.CRC16_MODBUS) + calculatedCRC := crc16.Checksum(respData, crcTable) + if calculatedCRC != receivedCRC { + return fmt.Errorf("CRC校验失败,计算值: %x,接收值: %x", calculatedCRC, receivedCRC) + } + + // 验证地址和功能码 + if respData[0] != address { + return fmt.Errorf("响应地址不符,期望: %x,实际: %x", address, respData[0]) + } + + // 处理正常/异常响应 + switch respData[1] { + case 0x10: + // 验证写入的地址和数量 + respStart := binary.BigEndian.Uint16(respData[2:4]) + respQuantity := binary.BigEndian.Uint16(respData[4:6]) + if respStart != startAddr || respQuantity != quantity { + return fmt.Errorf("响应参数不匹配,地址期望: %x 实际: %x,数量期望: %d 实际: %d", + startAddr, respStart, quantity, respQuantity) + } + return nil // 写入成功 + case 0x90: // 异常响应 + if len(respData) < 3 { + return fmt.Errorf("异常响应格式错误") + } + return fmt.Errorf("Modbus异常码: %x", respData[2]) + default: + return fmt.Errorf("未知功能码: %x", respData[1]) + } +} diff --git a/internal/library/ADL200/cmdOperate.go b/internal/library/ADL200/cmdOperate.go new file mode 100644 index 0000000..ca56c54 --- /dev/null +++ b/internal/library/ADL200/cmdOperate.go @@ -0,0 +1,55 @@ +package ADL200 + +import ( + "github.com/towgo/towgo/errors/terror" + "tgk-touch/internal/global" + "tgk-touch/internal/module/maincontrollerClient" +) + +// pushCmd 发送命令到设备并获取响应 +func pushCmd(commUid string, cmd []byte, detail string) ([]byte, error) { + var result []byte + err := maincontrollerClient.DeviceCallByDeviceID(commUid, "", "", cmd, &result, + func(deviceID, method string, params, result interface{}) (deviceType, cmdDetail, cmdDisplay, resultDisplay string) { + deviceType = "ADL200" + cmdDetail = detail + return + }) + if err != nil { + return nil, terror.Wrapf(err, "DeviceCallByDeviceID err ") + } + return result, nil +} + +// readCmd 读取保持寄存器 +func readCmd(commUid string, address byte, startAddr, quantity uint16, detail string) ([]uint16, error) { + cmd := ReadHoldingRegistersBuilder(address, startAddr, quantity) + g.Log().Debugf("ADL200 Modbus 下发 读取命令 = % X", cmd) + result, err := pushCmd(commUid, cmd, detail) + if err != nil { + return nil, terror.Wrapf(err, "下发[% X]", cmd) + } + g.Log().Debugf("ADL200 Modbus 响应 读取命令 = % X", result) + r, err := ReadHoldingRegisterParse(result, address, quantity) + if err != nil { + return nil, terror.Wrapf(err, "下发[% X],响应[% X]", cmd, result) + } + return r, nil +} + +// writeCmd 写入多个保持寄存器 +func writeCmd(commUid string, address byte, startAddr uint16, data []uint16, detail string) error { + writeData, q := WriteMultipleRegistersBuilder(address, startAddr, data) + g.Log().Debugf("ADL200 Modbus 下发 写入命令 = % X", writeData) + + result, err := pushCmd(commUid, writeData, detail) + if err != nil { + return err + } + g.Log().Debugf("ADL200 Modbus 响应 写入命令 = % X", result) + err = WriteMultipleRegistersParse(address, startAddr, q, result) + if err != nil { + return err + } + return nil +} diff --git a/internal/library/ADL400/cmd.go b/internal/library/ADL400/cmd.go new file mode 100644 index 0000000..5244143 --- /dev/null +++ b/internal/library/ADL400/cmd.go @@ -0,0 +1,129 @@ +package ADL400 + +import "fmt" + +var ( + H0000 = MeterModusCmd{startAddr: 0x00, quantity: 2} + H0002 = MeterModusCmd{startAddr: 0x02, quantity: 2} + H0004 = MeterModusCmd{startAddr: 0x04, quantity: 2} + H0006 = MeterModusCmd{startAddr: 0x06, quantity: 2} + H0008 = MeterModusCmd{startAddr: 0x08, quantity: 2} + H000A = MeterModusCmd{startAddr: 0x0A, quantity: 2, detail: "当前正向总有功电能"} + H000C = MeterModusCmd{startAddr: 0x0C, quantity: 2} + H000E = MeterModusCmd{startAddr: 0x0E, quantity: 2} + H0010 = MeterModusCmd{startAddr: 0x10, quantity: 2} + H0012 = MeterModusCmd{startAddr: 0x12, quantity: 2} + H0014 = MeterModusCmd{startAddr: 0x14, quantity: 2} + H0016 = MeterModusCmd{startAddr: 0x16, quantity: 2} + H0018 = MeterModusCmd{startAddr: 0x18, quantity: 2} + H001A = MeterModusCmd{startAddr: 0x1A, quantity: 2} + H001C = MeterModusCmd{startAddr: 0x1C, quantity: 2} + H001E = MeterModusCmd{startAddr: 0x1E, quantity: 2} + H0020 = MeterModusCmd{startAddr: 0x20, quantity: 2} + H0022 = MeterModusCmd{startAddr: 0x22, quantity: 2} + H0024 = MeterModusCmd{startAddr: 0x24, quantity: 2} + H0026 = MeterModusCmd{startAddr: 0x26, quantity: 2} + H0028 = MeterModusCmd{startAddr: 0x28, quantity: 2} + H002A = MeterModusCmd{startAddr: 0x2A, quantity: 2} + H002C = MeterModusCmd{startAddr: 0x2C, quantity: 2} + H002E = MeterModusCmd{startAddr: 0x2E, quantity: 2} + H0030 = MeterModusCmd{startAddr: 0x30, quantity: 2} + H0032 = MeterModusCmd{startAddr: 0x32, quantity: 2} + H0034 = MeterModusCmd{startAddr: 0x34, quantity: 2} + H0036 = MeterModusCmd{startAddr: 0x36, quantity: 2} + H0038 = MeterModusCmd{startAddr: 0x38, quantity: 2} + H003A = MeterModusCmd{startAddr: 0x3A, quantity: 2} + + H003C = MeterModusCmd{startAddr: 0x3C, quantity: 1, cmdType: 1, detail: "秒、分"} + H003D = MeterModusCmd{startAddr: 0x3D, quantity: 1, cmdType: 1, detail: "时、日"} + H003E = MeterModusCmd{startAddr: 0x3E, quantity: 1, cmdType: 1, detail: "月、年"} + H003F = MeterModusCmd{startAddr: 0x3F, quantity: 1, cmdType: 1, detail: "通信地址"} + + H0040 = MeterModusCmd{startAddr: 0x40, quantity: 1} + + H0041 = MeterModusCmd{startAddr: 0x41, quantity: 1, cmdType: 1} + H0042 = MeterModusCmd{startAddr: 0x42, quantity: 1, cmdType: 1} + H0043 = MeterModusCmd{startAddr: 0x43, quantity: 1, cmdType: 1} + H0044 = MeterModusCmd{startAddr: 0x44, quantity: 1, cmdType: 1} + H0045 = MeterModusCmd{startAddr: 0x45, quantity: 1, cmdType: 1} + H0046 = MeterModusCmd{startAddr: 0x46, quantity: 1, cmdType: 1} + H0061 = MeterModusCmd{startAddr: 0x61, quantity: 1, detail: "A相电压"} + H0062 = MeterModusCmd{startAddr: 0x62, quantity: 1, detail: "B相电压"} + H0063 = MeterModusCmd{startAddr: 0x63, quantity: 1, detail: "C相电压"} + H0064 = MeterModusCmd{startAddr: 0x64, quantity: 1, detail: "A相电流"} + H0065 = MeterModusCmd{startAddr: 0x65, quantity: 1, detail: "B相电流"} + H0066 = MeterModusCmd{startAddr: 0x66, quantity: 1, detail: "C相电流"} + H0067 = MeterModusCmd{startAddr: 0x67, quantity: 1, detail: "A相有功功率"} + H0068 = MeterModusCmd{startAddr: 0x68, quantity: 1, detail: "B相有功功率"} + H0069 = MeterModusCmd{startAddr: 0x69, quantity: 1, detail: "C相有功功率"} + H006A = MeterModusCmd{startAddr: 0x6A, quantity: 1, detail: "总有功功率"} + H0087 = MeterModusCmd{startAddr: 0x87, quantity: 2, detail: "A相正向有功电能"} + H0089 = MeterModusCmd{startAddr: 0x89, quantity: 2, detail: "B相正向有功电能"} + H008B = MeterModusCmd{startAddr: 0x8B, quantity: 2, detail: "C相正向有功电能"} + H0073 = MeterModusCmd{startAddr: 0x73, quantity: 2, detail: "A相功率因数"} + H0074 = MeterModusCmd{startAddr: 0x74, quantity: 2, detail: "B相功率因数"} + H0075 = MeterModusCmd{startAddr: 0x75, quantity: 2, detail: "C相功率因数"} + H0076 = MeterModusCmd{startAddr: 0x76, quantity: 2, detail: "总功率因数"} + H0121 = MeterModusCmd{startAddr: 0x0121, quantity: 1, cmdType: 1, detail: "日冻结时间 时"} + H0122 = MeterModusCmd{startAddr: 0x0121, quantity: 1, cmdType: 1, detail: "月冻结 日时"} + H6000 = MeterModusCmd{startAddr: 0x6000, quantity: 34} + H6001 = MeterModusCmd{startAddr: 0x6001, quantity: 1} + H6022 = MeterModusCmd{startAddr: 0x6022, quantity: 34} +) + +type MeterModusCmd struct { + startAddr uint16 + quantity uint16 + cmdType int // 0 R , 1 R/W + detail string +} + +func (mcc *MeterModusCmd) SetStartAddr(s uint16) { + mcc.startAddr = s +} +func (mcc *MeterModusCmd) GetStartAddr() uint16 { + return mcc.startAddr +} +func (mcc *MeterModusCmd) SetQuantity(q uint16) { + mcc.quantity = q +} +func (mcc *MeterModusCmd) GetQuantity() uint16 { + return mcc.quantity +} +func (mcc *MeterModusCmd) SetCmdType(t int) { + mcc.cmdType = t +} +func (mcc *MeterModusCmd) GetCmdType() int { + return mcc.cmdType +} + +func (mmc *MeterModusCmd) Read(commUid string, addr byte) ([]uint16, error) { + + return readCmd(commUid, addr, mmc.startAddr, mmc.quantity, "读取"+mmc.detail) + +} +func (mmc *MeterModusCmd) Write(commUid string, addr byte, data []uint16) error { + if mmc.cmdType == 0 { + return nil + } + return writeCmd(commUid, addr, mmc.startAddr, data, "写入"+mmc.detail) +} + +// 获取日冻结数据块地址 +func GetDailyBlockAddress(day int) (*MeterModusCmd, error) { + if day < 1 || day > 90 { + return nil, fmt.Errorf("invalid day value: %d (valid range 1-90)", day) + } + MMC := &MeterModusCmd{startAddr: 0x6000 + uint16(34*(day-1)), quantity: 34} + return MMC, nil +} + +// 获取月冻结数据块地址 +func GetMonthlyBlockAddress(month int) (*MeterModusCmd, error) { + if month < 1 || month > 48 { + return nil, fmt.Errorf("invalid month value: %d (valid range 1-48)", month) + } + MMC := &MeterModusCmd{startAddr: 0x7000 + uint16(34*(month-1)), quantity: 34} + + return MMC, nil +} diff --git a/internal/library/ADL400/cmdBuild.go b/internal/library/ADL400/cmdBuild.go new file mode 100644 index 0000000..cf9075b --- /dev/null +++ b/internal/library/ADL400/cmdBuild.go @@ -0,0 +1,146 @@ +package ADL400 + +import ( + "encoding/binary" + "fmt" + "github.com/sigurn/crc16" +) + +// readHoldingRegistersBuilder 读取保持寄存器 +func ReadHoldingRegistersBuilder(address byte, startAddr, quantity uint16) []byte { + // 构造Modbus-RTU请求帧 + request := make([]byte, 6) + request[0] = address // 从站地址 + request[1] = 0x03 // 功能码:读取保持寄存器 + binary.BigEndian.PutUint16(request[2:4], startAddr) // 起始地址 + binary.BigEndian.PutUint16(request[4:6], quantity) // 寄存器数量 + // 计算CRC校验 + crcTable := crc16.MakeTable(crc16.CRC16_MODBUS) + crc := crc16.Checksum(request[:6], crcTable) + request = append(request, byte(crc&0xFF), byte(crc>>8)) // 添加CRC,低字节在前 + + return request +} + +// 解析保持寄存器 +func ReadHoldingRegisterParse(result []byte, address byte, quantity uint16) ([]uint16, error) { + + // 检查响应最小长度 + if len(result) < 5 { + return nil, fmt.Errorf("响应长度不足: %d bytes", len(result)) + } + + // 提取数据和CRC + data := result[:len(result)-2] + receivedCRC := binary.LittleEndian.Uint16(result[len(result)-2:]) // 解析CRC(小端) + + // 校验CRC + crcTable := crc16.MakeTable(crc16.CRC16_MODBUS) + calculatedCRC := crc16.Checksum(data, crcTable) + if calculatedCRC != receivedCRC { + return nil, fmt.Errorf("CRC校验失败,计算值: %x,接收值: %x", calculatedCRC, receivedCRC) + } + + // 验证地址和功能码 + if data[0] != address { + return nil, fmt.Errorf("响应地址不符,期望: %x,实际: %x", address, data[0]) + } + + switch data[1] { + case 0x03: // 正常响应 + byteCount := data[2] + if len(data) < 3+int(byteCount) { + return nil, fmt.Errorf("响应数据不完整") + } + if int(byteCount) != 2*int(quantity) { + return nil, fmt.Errorf("数据长度不符,期望: %d,实际: %d", 2*quantity, byteCount) + } + + // 解析寄存器值 + registers := make([]uint16, quantity) + for i := 0; i < int(quantity); i++ { + registers[i] = binary.BigEndian.Uint16(data[3+2*i : 5+2*i]) + } + return registers, nil + + case 0x83: // 异常响应 + if len(data) < 3 { + return nil, fmt.Errorf("异常响应格式错误") + } + return nil, fmt.Errorf("Modbus异常码: %x", data[2]) + + default: + return nil, fmt.Errorf("未知功能码: %x", data[1]) + } +} + +// WriteMultipleRegisters 写入多个保持寄存器(功能码0x10) +func WriteMultipleRegistersBuilder(address byte, startAddr uint16, data []uint16) (writeData []byte, quantity uint16) { + // 构造Modbus-RTU请求帧 + quantity = uint16(len(data)) + byteCount := 2 * quantity + + request := make([]byte, 7+byteCount) + request[0] = address // 从站地址 + request[1] = 0x10 // 功能码:写多个寄存器 + binary.BigEndian.PutUint16(request[2:4], startAddr) // 起始地址 + binary.BigEndian.PutUint16(request[4:6], quantity) // 寄存器数量 + request[6] = byte(byteCount) // 字节数 + + // 填充数据(大端序) + for i, value := range data { + binary.BigEndian.PutUint16(request[7+2*i:9+2*i], value) + } + + // 计算CRC校验 + crcTable := crc16.MakeTable(crc16.CRC16_MODBUS) + crc := crc16.Checksum(request[:7+byteCount], crcTable) + request = append(request, byte(crc&0xFF), byte(crc>>8)) // 添加CRC(低字节在前) + + return request, quantity +} + +func WriteMultipleRegistersParse(address byte, startAddr uint16, quantity uint16, response []byte) error { + + // 检查响应最小长度 + if len(response) < 6 { + return fmt.Errorf("响应长度不足: %d bytes", len(response)) + } + + // 提取数据和CRC + respData := response[:len(response)-2] + receivedCRC := binary.LittleEndian.Uint16(response[len(response)-2:]) + + // 校验CRC + crcTable := crc16.MakeTable(crc16.CRC16_MODBUS) + calculatedCRC := crc16.Checksum(respData, crcTable) + if calculatedCRC != receivedCRC { + return fmt.Errorf("CRC校验失败,计算值: %x,接收值: %x", calculatedCRC, receivedCRC) + } + + // 验证地址和功能码 + if respData[0] != address { + return fmt.Errorf("响应地址不符,期望: %x,实际: %x", address, respData[0]) + } + + // 处理正常/异常响应 + switch respData[1] { + case 0x10: + // 验证写入的地址和数量 + respStart := binary.BigEndian.Uint16(respData[2:4]) + respQuantity := binary.BigEndian.Uint16(respData[4:6]) + if respStart != startAddr || respQuantity != quantity { + return fmt.Errorf("响应参数不匹配,地址期望: %x 实际: %x,数量期望: %d 实际: %d", + startAddr, respStart, quantity, respQuantity) + } + //log.Print("Modbus:写入成功") + return nil // 写入成功 + case 0x90: // 异常响应 + if len(respData) < 3 { + return fmt.Errorf("异常响应格式错误") + } + return fmt.Errorf("Modbus异常码: %x", respData[2]) + default: + return fmt.Errorf("未知功能码: %x", respData[1]) + } +} diff --git a/internal/library/ADL400/cmdOperate.go b/internal/library/ADL400/cmdOperate.go new file mode 100644 index 0000000..4d514ed --- /dev/null +++ b/internal/library/ADL400/cmdOperate.go @@ -0,0 +1,53 @@ +package ADL400 + +import ( + "github.com/towgo/towgo/errors/terror" + "tgk-touch/internal/global" + "tgk-touch/internal/module/maincontrollerClient" +) + +func pushCmd(commUid string, cmd []byte, detail string) ([]byte, error) { + + var result []byte + err := maincontrollerClient.DeviceCallByDeviceID(commUid, "", "", cmd, &result, + func(deviceID, method string, params, result interface{}) (deviceType, cmdDetail, cmdDisplay, resultDisplay string) { + deviceType = "ADL400" + cmdDetail = detail + return + }) + if err != nil { + return nil, terror.Wrapf(err, "DeviceCallByDeviceID err ") + } + return result, nil +} + +func readCmd(commUid string, address byte, startAddr, quantity uint16, detail string) ([]uint16, error) { + cmd := ReadHoldingRegistersBuilder(address, startAddr, quantity) + g.Log().Debugf("Modbus 下发 读取命令 = % X", cmd) + result, err := pushCmd(commUid, cmd, detail) + if err != nil { + return nil, terror.Wrapf(err, "下发[% X]", cmd) + } + g.Log().Debugf("Modbus 响应 读取命令 = % X", result) + r, err := ReadHoldingRegisterParse(result, address, quantity) + if err != nil { + return nil, terror.Wrapf(err, "下发[% X],响应[% X]", cmd, result) + } + return r, nil +} + +func writeCmd(commUid string, address byte, startAddr uint16, data []uint16, detail string) error { + writeData, q := WriteMultipleRegistersBuilder(address, startAddr, data) + g.Log().Debugf("Modbus 下发 写入命令 = % X", writeData) + + result, err := pushCmd(commUid, writeData, detail) + if err != nil { + return err + } + g.Log().Debugf("Modbus 响应 写入命令 = % X", result) + err = WriteMultipleRegistersParse(address, startAddr, q, result) + if err != nil { + return err + } + return nil +} diff --git a/internal/library/m9z/m9z.go b/internal/library/m9z/m9z.go new file mode 100644 index 0000000..8de756f --- /dev/null +++ b/internal/library/m9z/m9z.go @@ -0,0 +1,74 @@ +package m9z + +type M9zCtrl byte + +const ( + // 手动控制 + manual M9zCtrl = 0x01 + // 日期时间 + deviceTime M9zCtrl = 0x02 + // 获取设备输出状态参数 + deviceOutStatus M9zCtrl = 0x04 + //经纬度参数 + lgnLat M9zCtrl = 0x05 + // 日出日落时间偏差调节 + sunRiseSet M9zCtrl = 0x06 + // 模式选择 + mode M9zCtrl = 0x0B + // 获取设备的状态 + deviceStatus M9zCtrl = 0x10 + // 获取或设置设备的配置 CMD = 0x11 ,IDX = 0x00 + deviceConfig M9zCtrl = 0x11 + // (9)获取日出日落时间 CMD = 0x12 ,IDX = 0x00 + sunRiseTime M9zCtrl = 0x12 + // (10)获取回路分控器的参数CMD = 0x17 ,IDX = 0x00~0x09 + subLoopParameters M9zCtrl = 0x17 + //(12)保存配置参数 CMD = 0x7F ,IDX = 0x00 + save M9zCtrl = 0x7F + + // 重启设备 CMD = 0x7E ,IDX = 0x00 + restart M9zCtrl = 0x7E + + // (16)优化任务开始数据 CMD = 0x13 ,IDX = 0x00~0x02 + startTask M9zCtrl = 0x13 + // (17)优化中间任务数据 CMD = 0x14 ,IDX = 0x00~0x02 + middleTask M9zCtrl = 0x14 + // (18)优化任务结束数据 CMD = 0x15 ,IDX = 0x00~0x02 + stopTask M9zCtrl = 0x15 + // 蓝牙配置协议 + bluetooth M9zCtrl = 0x16 + eraseSector M9zCtrl = 0x0D // 擦除扇区 + firmwareData M9zCtrl = 0x0E // 下发固件数据 + lockFirmware M9zCtrl = 0x0F // 固件加锁 + versionCmd M9zCtrl = 0x0E + // (20) 实际通断情况查询 CMD = 0x19 + relayStatus M9zCtrl = 0x19 +) + +type IReadError interface { + Error() string +} +type ReadError struct { + msg string +} + +func (e *ReadError) Error() string { + return e.msg +} +func NewReadError(msg string) *ReadError { + return &ReadError{ + msg: msg, + } +} + +func GetRestartCmd() []byte { + wIdx := byte(0x00) + + writeCmd := WriteCommand{ + Instruction: byte(restart), + Index: wIdx, + Data: nil, + } + return BuildWriteCommand(writeCmd) + +} diff --git a/internal/library/m9z/m9z_Bluetooth.go b/internal/library/m9z/m9z_Bluetooth.go new file mode 100644 index 0000000..687ca18 --- /dev/null +++ b/internal/library/m9z/m9z_Bluetooth.go @@ -0,0 +1,120 @@ +package m9z + +import ( + "bytes" + "fmt" + "github.com/towgo/towgo/errors/terror" +) + +// 对应C的db_ble_extern_t结构体(简化版,使用bool型Enable) +type BluetoothConfig struct { + Enable bool // 离线监控使能(false=禁止,true=允许/0xAA) + OfflineMin uint8 // 离线时间(分钟数) + BleName string // 蓝牙名称(最大20字节) 不支持中文 +} + +func BluetoothRead(deviceId string) (*BluetoothConfig, error) { + // 1. 读取指令 + cmd, err := ReadCmd(deviceId, bluetooth, "设备蓝牙配置读取") + if err != nil { + errMsg := fmt.Sprintf("读取设备%s蓝牙配置失败: %v", deviceId, err) + + return nil, terror.New(errMsg) + } + + data := cmd.Data + + // 2. 校验基础长度(至少2字节:离线监控+离线时间) + const minDataLen = 2 + if len(data) < minDataLen { + errMsg := fmt.Sprintf("蓝牙配置数据长度不足,至少需要%d字节,实际收到%d字节", minDataLen, len(data)) + + return nil, terror.New(errMsg) + } + + // 3. 解析基础字段(固定2字节) + var bleConfig BluetoothConfig + bleConfig.Enable = data[0] == 0xAA // 0xAA对应true(允许),其他为false(禁止) + bleConfig.OfflineMin = data[1] // 离线时间 + + // 4. 解析蓝牙名称(动态长度,最多20字节) + nameBytes := data[2:] // 取基础字段后的所有字节 + if len(nameBytes) > 20 { + nameBytes = nameBytes[:20] // 超过20字节则截断 + } + // 去掉名称末尾的空字符(设备返回的结束符) + bleConfig.BleName = string(bytes.TrimRight(nameBytes, "\x00")) + return &bleConfig, nil +} + +// BluetoothWrite 写入蓝牙配置到设备(修复所有核心问题) +func BluetoothWrite(deviceId string, bleConfig *BluetoothConfig) error { + // 1. 精准参数校验(拆分判断,提示语匹配) + if bleConfig == nil { + errMsg := "蓝牙配置结构体为nil,无法写入" + + return terror.New(errMsg) + } + nameBytes := []byte(bleConfig.BleName) + if len(nameBytes) > 20 { + errMsg := fmt.Sprintf("蓝牙名称超长,最大支持20字节,实际传入%d字节", len(nameBytes)) + + return terror.New(errMsg) + } + // 校验1:是否包含中文/非ASCII字符 + if hasChinese(bleConfig.BleName) { + errMsg := fmt.Sprintf("设备[%s]蓝牙名称包含中文/非ASCII字符,仅支持英文/数字/符号等ASCII字符", deviceId) + + return terror.New(errMsg) + } + + // 2. 构造缓冲区:2字节基础字段 + 动态长度蓝牙名称 + buf := make([]byte, 2) // 先初始化基础字段缓冲区 + // 编码离线监控使能(Byte0:0=禁止,0xAA=允许) + if bleConfig.Enable { + buf[0] = 0xAA + } else { + buf[0] = 0x00 + } + // 编码离线时间(Byte1) + buf[1] = bleConfig.OfflineMin + + // 追加蓝牙名称(仅追加一次,修复重复追加问题) + buf = append(buf, nameBytes...) + + // 调试日志:打印最终缓冲区(长度+十六进制) + //g.Log().Debugf("Bluetooth Write raw data: len=%d , hex=% X", len(buf), buf) + + // 3. 发送写入命令 + writeResp, err := WriteCmd(deviceId, bluetooth, buf, "设备蓝牙配置写入") + if err != nil { + errMsg := fmt.Sprintf("设备[%s]蓝牙配置写入命令调用失败", deviceId) + return terror.Wrapf(err, errMsg) + } + + // 4. 检查写入响应状态(修复err为nil的问题) + if !writeResp.Success { + errMsg := fmt.Sprintf("设备[%s]蓝牙配置写入失败,响应状态为失败", deviceId) + + // 手动构造错误,避免Wrapf传入nil + return terror.New(errMsg) + } + + // 5. 写入成功日志(修复切片越界问题:用buf[2:]而非固定22) + //bleNameStr := string(bytes.TrimRight(buf[2:], "\x00")) + //g.Log().Infof("设备%s蓝牙配置写入成功,离线监控使能:%v,离线时间:%d分钟,蓝牙名称:%s", + // deviceId, bleConfig.Enable, bleConfig.OfflineMin, bleNameStr) + + return nil +} + +// hasChinese 辅助函数:判断字符串是否包含中文(或非ASCII字符) +func hasChinese(s string) bool { + for _, r := range s { + // 中文Unicode范围:0x4E00-0x9FFF,同时拦截所有>127的非ASCII字符 + if r > 127 || (r >= 0x4E00 && r <= 0x9FFF) { + return true + } + } + return false +} diff --git a/internal/library/m9z/m9z_DeviceConfig.go b/internal/library/m9z/m9z_DeviceConfig.go new file mode 100644 index 0000000..f484a10 --- /dev/null +++ b/internal/library/m9z/m9z_DeviceConfig.go @@ -0,0 +1,99 @@ +package m9z + +import ( + "encoding/binary" + "fmt" + "github.com/towgo/towgo/errors/terror" + "tgk-touch/internal/global" +) + +type DeviceConfig struct { + Latitude float64 // 纬度(实际值) + Longitude float64 // 经度(实际值) + TimezoneOffset float64 // 时区偏移(实际值,单位小时,如 UTC+8 为 8.0) + SunriseAdjust int // 日出偏差调整(分钟) + SunsetAdjust int // 日落偏差调整(分钟) + ProgrammingMode uint8 // 编程模式 0-2 + RS485Address uint8 // 485地址 (默认0x01) +} + +// todo 和实际有出入 +func DeviceConfigRead(deviceId string) (*DeviceConfig, error) { + cmd, err := ReadCmd(deviceId, deviceConfig, "设备配置读取") + if err != nil { + return nil, err + } + data := cmd.Data + + // 数据长度校验(至少18字节) + if len(data) < 18 { + return nil, fmt.Errorf("invalid config data length: %d", len(data)) + } + + dc := &DeviceConfig{} + + // 1. 解析纬度(Byte0-3,小端序int32) + latInt := float64(binary.LittleEndian.Uint32(data[0:4])) + dc.Latitude = float64(latInt) / 1_000_000 + + // 2. 解析经度(Byte4-7,小端序int32) + lngInt := float64(binary.LittleEndian.Uint32(data[4:8])) + dc.Longitude = float64(lngInt) / 1_000_000 + + // 3. 解析时区偏移(Byte8-11,小端序int32) + tzInt := float64(binary.LittleEndian.Uint32(data[8:12])) + dc.TimezoneOffset = float64(tzInt) / 1_000_000 + + // 4. 日出/日落调整(Byte12-13) + g.Log().Infof("日出日落偏差2% X", data[12:14]) + dc.SunriseAdjust = int(data[12]) + dc.SunsetAdjust = int(data[13]) + + // 5. 编程模式(Byte16) + dc.ProgrammingMode = data[16] + + // 6. 485地址(Byte17) + dc.RS485Address = data[17] + + return dc, nil +} + +func DeviceConfigWrite(deviceId string, dc *DeviceConfig) error { + // 构造18字节数据缓冲区 + buf := make([]byte, 18) + + // 1. 编码纬度(扩大1,000,000倍) + latInt := int32(dc.Latitude * 1_000_000) + binary.LittleEndian.PutUint32(buf[0:4], uint32(latInt)) + + // 2. 编码经度 + lngInt := int32(dc.Longitude * 1_000_000) + binary.LittleEndian.PutUint32(buf[4:8], uint32(lngInt)) + + // 3. 编码时区偏移 + tzInt := int32(dc.TimezoneOffset * 1_000_000) + binary.LittleEndian.PutUint32(buf[8:12], uint32(tzInt)) + + // 4. 日出/日落调整 + buf[12] = byte(dc.SunriseAdjust) + buf[13] = byte(dc.SunsetAdjust) + + // 5. 备用字段(Byte14-15)填充0 + binary.LittleEndian.PutUint16(buf[14:16], 0) + + // 6. 编程模式 + buf[16] = dc.ProgrammingMode + + // 7. 485地址 + buf[17] = dc.RS485Address + + // 发送写入命令 + writeResp, err := WriteCmd(deviceId, deviceConfig, buf, "设备配置写入") + if err != nil { + return err + } + if !writeResp.Success { + return terror.New("写入失败") + } + return nil +} diff --git a/internal/library/m9z/m9z_DeviceOutStatus.go b/internal/library/m9z/m9z_DeviceOutStatus.go new file mode 100644 index 0000000..95249cf --- /dev/null +++ b/internal/library/m9z/m9z_DeviceOutStatus.go @@ -0,0 +1,39 @@ +package m9z + +import ( + "fmt" +) + +// 读取设备输出状态参数 +type OutStatus struct { + BackupByte2 byte `json:"backup_byte1"` //备用字节下标2 + Loops []Loop `json:"loops"` //回路数据 +} + +func DeviceOutStatusRead(deviceId string) (*OutStatus, error) { + out := &OutStatus{} + readResp, err := ReadCmd(deviceId, deviceOutStatus, "读取设备输出状态参数") + if err != nil { + return nil, err + } + + // 解析10路的继电器动作状态 + relayBits := BytesToUint16LE(readResp.Data[0:2]) + //log.Printf("继电器状态(二进制): %016b,% X", relayBits, readResp.Data[0:2]) // 补零显示16位 + loops := make([]Loop, 10) + for i := 0; i < 10; i++ { + var loop Loop + bit := (relayBits >> i) & 0x01 // 取第i位的值 + loop.IsOpen = bit == 1 + loop.Index = i + loop.Name = fmt.Sprintf("#%+v", i+1) + loops[i] = loop + } + out.BackupByte2 = readResp.Data[2] + pwms := readResp.Data[3 : len(readResp.Data)-1] + for index, pwm := range pwms { + loops[index].Pwm = hexToPercent(pwm) + } + out.Loops = loops + return out, err +} diff --git a/internal/library/m9z/m9z_DeviceStatus.go b/internal/library/m9z/m9z_DeviceStatus.go new file mode 100644 index 0000000..152a97a --- /dev/null +++ b/internal/library/m9z/m9z_DeviceStatus.go @@ -0,0 +1,60 @@ +package m9z + +import ( + "encoding/binary" + "fmt" + "tgk-touch/internal/global" + "time" +) + +type DeviceStatus struct { + Mode uint // 模式 (0=自动, 1=手动) + DeviceTime time.Time // 系统时间 (UTC秒数) + Relays [10]bool // 10路继电器状态 (true=动作) + Optocoupler [8]bool // 8路光耦输入状态 (true=触发) + PWMOutputs [10]uint8 // 10路PWM输出值 (0-100) +} + +func DeviceStatusRead(deviceId string) (*DeviceStatus, error) { + cmd, err := ReadCmd(deviceId, deviceStatus, "读取设备的状态") + if err != nil { + return nil, err + } + data := cmd.Data + + // 数据长度校验 (至少需要18字节) + if len(data) < 18 { + return nil, fmt.Errorf("invalid data length: expect 18 bytes, got %d", len(data)) + } + + ds := &DeviceStatus{} + + // 1. 解析工作模式 (Byte0) + ds.Mode = uint(data[0]) + + // 2. 解析系统时间 (Byte1~4 小端序uint32) + unixSec := int64(binary.LittleEndian.Uint32(data[1:5])) + ds.DeviceTime = time.Unix(unixSec, 0).UTC() + + // 3. 解析10路继电器状态 (Byte5~6 小端序uint16) + //relayBits := binary.LittleEndian.Uint16(data[5:7]) + relayBits := binary.LittleEndian.Uint16(data[5:7]) + g.Log().Debugf("%s 解析10路继电器状态(二进制): %016b ,% X", deviceId, relayBits, data[5:7]) // 补零显示16位 + + for i := 0; i < 10; i++ { + ds.Relays[i] = (relayBits & (1 << i)) != 0 + } + + // 4. 解析8路光耦输入 (Byte7) + optoByte := data[7] + for i := 0; i < 8; i++ { + ds.Optocoupler[i] = (optoByte & (1 << i)) != 0 + } + + // 5. 解析10路PWM输出 (Byte8~17) + for i := 0; i < 10; i++ { + ds.PWMOutputs[i] = uint8(data[8+i]) + } + + return ds, nil +} diff --git a/internal/library/m9z/m9z_DeviceTime.go b/internal/library/m9z/m9z_DeviceTime.go new file mode 100644 index 0000000..ae3e8ec --- /dev/null +++ b/internal/library/m9z/m9z_DeviceTime.go @@ -0,0 +1,43 @@ +package m9z + +// 时间获取 +import ( + "encoding/binary" + "fmt" + "log" + "time" +) + +func DeviceTimeRead(deviceId string) (time.Time, int64, error) { + readResp, err := ReadCmd(deviceId, deviceTime, "读取日期时间") + if err != nil { + return time.Time{}, 0, err + } + u := binary.LittleEndian.Uint32(readResp.Data) + return time.Unix(int64(u), 0), int64(u), nil +} + +// DeviceTimeWrite 将当前时间写入设备 +func DeviceTimeWrite(deviceId string, t time.Time) error { + + // 2. 将时间转换为Unix时间戳(秒) + unixTime := uint32(t.Unix()) + + // 3. 准备写入数据(小端序4字节) + timeData := make([]byte, 4) + binary.LittleEndian.PutUint32(timeData, unixTime) + + // 5. 发送命令 + writeResp, err := WriteCmd(deviceId, deviceTime, timeData, "将当前时间写入设备") + if err != nil { + return err + } + + // 6. 验证响应 + if !writeResp.Success { + return fmt.Errorf("设备返回写入失败 ") + } + + log.Printf("成功写入设备时间: %s", t.Format("2006-01-02 15:04:05")) + return nil +} diff --git a/internal/library/m9z/m9z_Firmware.go b/internal/library/m9z/m9z_Firmware.go new file mode 100644 index 0000000..50f3dcb --- /dev/null +++ b/internal/library/m9z/m9z_Firmware.go @@ -0,0 +1,411 @@ +package m9z + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "fmt" + "github.com/towgo/towgo/errors/terror" + "io" + "net/http" + "os" + "path/filepath" + g "tgk-touch/internal/global" + "time" +) + +type FirmwareReadBlock struct { + Address uint32 + Length uint16 + Data []byte +} + +// FirmwareRead 读取设备指定地址和长度的固件数据 +// 参数: +// - deviceId: 设备ID +// - address: 读取起始地址(32位,如0x08000000) +// - length: 读取数据长度(16位,如512字节) +// +// 返回: 读取到的固件数据及可能的错误 +func FirmwareRead(deviceId string, address uint32, length uint16) (*FirmwareReadBlock, error) { + // 1. 构建读取固件的payload + // 结构说明: [指令类型(1字节)][操作类型(1字节)][索引(1字节)][数据长度(1字节)][地址(4字节小端)][读取长度(2字节小端)] + payload := make([]byte, 0, 10) // 固定10字节payload + payload = append(payload, byte(firmwareData)) // 指令类型: 0x0E (固件数据相关) + payload = append(payload, CmdRead) // 操作类型: 0x01 (读取) + payload = append(payload, 0x00) // 索引: 固定0x00 + + // 数据长度字段: 后续地址(4字节) + 长度(2字节) = 6字节,故为0x06 + payload = append(payload, 0x06) + + // 地址字段: 32位小端序 + addrBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(addrBytes, address) + payload = append(payload, addrBytes...) + + // 读取长度字段: 16位小端序 + lenBytes := make([]byte, 2) + binary.LittleEndian.PutUint16(lenBytes, length) + payload = append(payload, lenBytes...) + + // 2. 计算校验和 + checksum := calCheckSum(payload) + + // 3. 构建完整帧: [帧头][payload][校验和][帧尾] + frame := []byte{FrameHeader} // 帧头: 0xEE + frame = append(frame, payload...) // 加入payload + frame = append(frame, checksum) // 加入校验和 + frame = append(frame, FrameFooter) // 帧尾: 0xFF + + // 4. 发送命令并获取响应 + respData, err := pushCommand(deviceId, frame, "读取固件数据") + if err != nil { + return nil, terror.Wrapf(err, "设备[%s]读取固件数据失败(地址:0x%X, 长度:%d)", deviceId, address, length) + } + + // 5. 解析响应帧 + frameResp, err := ParseBlockFrame(respData) + if err != nil { + return nil, terror.Wrapf(err, "设备[%s]解析固件响应失败", deviceId) + } + block := &FirmwareReadBlock{} + block.Address = binary.LittleEndian.Uint32(frameResp.Data[0:4]) + block.Length = BytesToUint16LE(frameResp.Data[4:6]) + block.Data = frameResp.Data[6:] + + // 校验返回数据长度是否匹配请求长度 + if block.Length != uint16(length) { + return nil, fmt.Errorf("设备[%s]固件数据长度不匹配(期望:%d, 实际:%d)", deviceId, length, len(block.Data)) + } + + return block, nil +} + +// 解析接收帧 +func ParseBlockFrame(data []byte) (*ReadResponse, error) { + g.Log().Debugf("ParseFrame:[% X]", data) + s := "" + for i, d := range data { + if i == 0 { + + } + s += fmt.Sprintf("0x%X,", d) + } + //g.Log().Debugf("\n ParseFrame:{%s} ", s) + // 基本长度检查 + if len(data) < 5 { + return nil, + NewReadError("frame too short") + } + + // 验证帧头帧尾 + if data[0] != FrameHeader || data[len(data)-1] != FrameFooter { + return nil, NewReadError("invalid frame header or footer") + } + + // 提取有效载荷和校验和 + payload := data[1 : len(data)-2] + receivedCS := data[len(data)-2] + + // 计算校验和 + if calCheckSum(payload) != receivedCS { + return nil, NewReadError("checksum mismatch") + } + + // 解析指令类型 + if len(payload) < 2 { + return nil, NewReadError("invalid payload length") + } + + instruction := payload[0] + _ = payload[1] + + // 判断是否是响应帧(D7位是否置1) + _ = (instruction & ResponseFlag) != 0 + + return parseReadBlockResponse(payload) + +} + +// GenerateFirmwareShortID 生成固件的短唯一标识 +// 参数: +// +// firmware - 固件字节数组 +// +// 返回: +// +// 短唯一字符串、错误(长度非法时) +func GenerateFirmwareShortID(firmware []byte) (shortID string) { + length := 8 + // 1. 校验长度合法性 + //if length <= 0 || length > 43 { // Base64URL编码SHA256最大43位,十六进制最大64位 + // return "", fmt.Errorf("短ID长度需在1~43之间(推荐8~16)") + //} + + // 2. 计算固件的SHA256哈希(核心:内容变则哈希变) + firmware = append(firmware, byte(len(firmware))) + hash := sha256.Sum256(firmware) + hashBytes := hash[:] // 32字节的哈希值 + + // 3. Base64URL编码(无特殊字符,比十六进制更短) + // Base64URL:去掉+、/,替换为-、_,不补=,适合URL/文件名 + base64URL := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hashBytes) + + // 4. 截取指定长度(前length位),保证短小且唯一 + shortID = base64URL[:length] + + return shortID +} + +// 固件升级起始地址 +const FirmwareStartAddr = 0x00000000 + +// FirmwareUpgrade 设备固件完整升级流程 +func FirmwareUpgrade(deviceId string, firmware []byte, startAddr uint32) (uniqueId string, addr uint32, err error) { + blockSize := 512 + initialOffset := int(startAddr - FirmwareStartAddr) + addr = startAddr // 起始地址 + + uniqueId = GenerateFirmwareShortID(firmware) + if startAddr == FirmwareStartAddr { + // 1. 擦除扇区 + g.Log().Debug("开始擦除分区") + _, err = EraseSector(deviceId) + if err != nil { + return uniqueId, addr, fmt.Errorf("擦除扇区阶段失败: %v", err) + } + + } else { + /* // 不是起始就要判断完整性 + blockData := firmware[initialOffset : initialOffset+blockSize] + blockHash := sha256.Sum256(blockData) + frimwareVer := hex.EncodeToString(blockHash[:]) + read, err := FirmwareRead(deviceId, startAddr, uint16(blockSize)) + if err != nil { + return uniqueId, addr, terror.Wrapf(err, "验证设备上最新固件包失败") + } + deviceHash := sha256.Sum256(read.Data) + deviceVer := hex.EncodeToString(deviceHash[:]) + if deviceVer != frimwareVer { + os.WriteFile("./blockData.txt", []byte(fmt.Sprintf("% X", blockData)), 0666) + os.WriteFile("./readData.txt", []byte(fmt.Sprintf("% X", read.Data)), 0666) + return uniqueId, addr, terror.Newf("设备最新不完整,建议擦除分区重试 %s != %s %d %d", frimwareVer, deviceVer, len(blockData), len(read.Data)) + }*/ + } + + // 2. 分块下发固件数据(每块512字节,不足补零) + totalLen := len(firmware) + blockCount := 0 + // 3. 计算起始地址对应的固件偏移量(核心:地址→偏移量映射) + g.Log().Debugf("起始地址0x%X对应固件偏移量: %d字节", startAddr, initialOffset) + for offset := initialOffset; offset < totalLen; offset += blockSize { + blockCount++ + // 计算当前块数据 + end := offset + blockSize + if end > totalLen { + end = totalLen + } + blockData := firmware[offset:end] + + // 补零逻辑:如果块大小不足512字节,自动补零到512字节 + if len(blockData) < blockSize { + paddingLen := blockSize - len(blockData) + blockData = append(blockData, make([]byte, paddingLen)...) + } + + blockLen := uint16(len(blockData)) // 补零后长度应为512(最后一块可能补零后等于blockSize) + + // 计算扩展长度(总长度高8位)和数据长度(低8位) + totalBlockDataLen := 6 + int(blockLen) // 1(ext) + 4(addr) + 2(len) + dataLen + extDataLen := byte(totalBlockDataLen >> 8) + + // 下发当前块(带重试机制) + _, err = FirmwareDataWrite(deviceId, extDataLen, addr, blockLen, blockData) + /* + for i := 0; i < 100; i++ { + _, err = FirmwareDataWrite(deviceId, extDataLen, addr, blockLen, blockData) + if err == nil { + break + } else { + err = fmt.Errorf("下发固件块(偏移%d)失败: %v", offset, err) + g.Log().Error(err.Error()) + } + time.Sleep(3 * time.Second) + }*/ + if err != nil { + return uniqueId, addr, err + } + + // 更新地址(按实际补零后的块长度递增) + addr += uint32(blockLen) + + } + + // 3. 加锁固件 + _, err = LockFirmware(deviceId) + if err != nil { + return uniqueId, addr, fmt.Errorf("固件加锁阶段失败: %v", err) + } + + // 4. 重启设备完成升级 + if err := Restart(deviceId); err != nil { + return uniqueId, addr, fmt.Errorf("重启设备失败: %v", err) + } + + return uniqueId, addr, err +} + +// EraseSector 擦除设备扇区 +// 指令: CMD=0x0D, IDX=0x00 +func EraseSector(deviceId string) (*WriteResponse, error) { + // 固定数据格式: 04 77 88 99 00(参考示例指令) + data := []byte{0x04, 0x77, 0x88, 0x99, 0x00} + + // 发送写入命令 + resp, err := WriteCmd(deviceId, eraseSector, data, "擦除扇区", UpdateMarker) + if err != nil { + return nil, terror.Wrapf(err, "设备[%s]擦除扇区失败", deviceId) + } + + // 验证响应状态 + if !resp.Success { + return nil, fmt.Errorf("设备[%s]擦除扇区响应失败", deviceId) + } + + return resp, nil +} + +// FirmwareDataWrite 下发固件数据到设备 +// 参数: +// - deviceId: 设备ID +// - extDataLen: 扩展数据长度(高8位,与dataLen组成16位总长度) +// - addr: 写入地址(4字节) +// - writeLen: 写入数据长度(2字节) +// - fileData: 实际固件数据 +func FirmwareDataWrite(deviceId string, extDataLen byte, addr uint32, writeLen uint16, fileData []byte) (*WriteResponse, error) { + // 1. 校验参数合法性 + totalDataLen := uint16(extDataLen)<<8 | uint16(len(fileData)+6) // 7 = 1(ext) + 4(addr) + 2(writeLen) + if totalDataLen != uint16(extDataLen)<<8|uint16(len(fileData)+6) { + return nil, fmt.Errorf("数据长度不匹配,扩展长度0x%02X,实际总长度0x%04X", extDataLen, len(fileData)+7) + } + if int(writeLen) != len(fileData) { + return nil, fmt.Errorf("写入长度与文件数据长度不匹配(%d vs %d)", writeLen, len(fileData)) + } + + // 2. 构造升级数据 + upgradeData := make([]byte, 0, 6+len(fileData)) + upgradeData = append(upgradeData, extDataLen) // 扩展数据长度(1字节) + + // 写入地址(4字节,小端序) + addrBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(addrBytes, addr) + upgradeData = append(upgradeData, addrBytes...) + + // 写入长度(2字节,小端序) + lenBytes := make([]byte, 2) + binary.LittleEndian.PutUint16(lenBytes, writeLen) + + upgradeData = append(upgradeData, lenBytes...) + + // 固件文件数据 + upgradeData = append(upgradeData, fileData...) + + // 3. 构造命令数据(数据长度字段 + 升级数据) + dataLen := byte(len(upgradeData) - 1&0xFF) // 数据长度(低8位) + cmdData := append([]byte{dataLen}, upgradeData...) + + // 4. 发送写入命令 + resp, err := WriteCmd(deviceId, firmwareData, cmdData, "下发固件数据", UpdateMarker) + if err != nil { + return nil, terror.Wrapf(err, "\n设备[%s]下发固件数据失败", deviceId) + } + + if !resp.Success { + return nil, fmt.Errorf("设备[%s]固件数据写入响应失败", deviceId) + } + + return resp, nil +} + +// LockFirmware 固件数据下发完成后加锁 +// 指令: CMD=0x0F, IDX=0x00 +func LockFirmware(deviceId string) (*WriteResponse, error) { + // 无数据 payload(参考示例指令:数据长度0x00) + resp, err := WriteCmd(deviceId, lockFirmware, []byte{00}, "固件加锁") + if err != nil { + return nil, terror.Wrapf(err, "设备[%s]固件加锁失败", deviceId) + } + + if !resp.Success { + return nil, fmt.Errorf("设备[%s]固件加锁响应失败", deviceId) + } + + return resp, nil +} + +// GetFirmwareFromLocal 从本地文件读取固件 +// 参数: localPath 本地固件文件路径(如 "./firmware_v1.2.bin") +func GetFirmwareFromLocal(localPath string) ([]byte, error) { + // 校验路径合法性 + if localPath == "" { + return nil, terror.New("本地固件路径不能为空") + } + absPath, err := filepath.Abs(localPath) + if err != nil { + return nil, terror.Wrapf(err, "读取固件绝对路径失败(%s)", localPath) + } + + // 检查文件是否存在 + fileInfo, err := os.Stat(absPath) + if err != nil { + return nil, terror.Wrapf(err, "固件文件不存在或无法访问(%s)", absPath) + } + if fileInfo.IsDir() { + return nil, terror.Newf("路径指向目录,不是固件文件(%s)", absPath) + } + + // 读取文件内容 + firmwareData, err := os.ReadFile(absPath) + if err != nil { + return nil, terror.Wrapf(err, "读取固件文件失败(%s)", absPath) + } + + g.Log().Infof("成功从本地读取固件,路径:%s,大小:%d字节", absPath, len(firmwareData)) + return firmwareData, nil +} + +// GetFirmwareFromURL 从网络URL下载固件 +// 参数: url 固件下载地址(如 "http://example.com/firmware_v1.2.bin") +func GetFirmwareFromURL(url string) ([]byte, error) { + // 校验URL合法性 + if url == "" { + return nil, terror.New("固件下载URL不能为空") + } + if filepath.Ext(url) != ".bin" { + g.Log().Warnf("固件URL后缀非.bin,可能不是有效固件文件(%s)", url) + } + + // 发送HTTP GET请求(设置超时时间) + client := &http.Client{ + Timeout: 30 * time.Second, // 30秒超时 + } + resp, err := client.Get(url) + if err != nil { + return nil, terror.Wrapf(err, "固件下载请求失败(%s)", url) + } + defer resp.Body.Close() // 确保响应体关闭 + + // 校验响应状态 + if resp.StatusCode != http.StatusOK { + return nil, terror.Newf("固件下载失败,HTTP状态码:%d(%s)", resp.StatusCode, url) + } + + // 读取响应内容 + firmwareData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, terror.Wrapf(err, "读取固件响应内容失败(%s)", url) + } + + g.Log().Infof("成功从网络下载固件,URL:%s,大小:%d字节", url, len(firmwareData)) + return firmwareData, nil +} diff --git a/internal/library/m9z/m9z_LgnLat.go b/internal/library/m9z/m9z_LgnLat.go new file mode 100644 index 0000000..3d0460e --- /dev/null +++ b/internal/library/m9z/m9z_LgnLat.go @@ -0,0 +1,103 @@ +package m9z + +import ( + "encoding/binary" + "fmt" + "github.com/towgo/towgo/errors/terror" + "math" + "time" +) + +type LgnLatData struct { + Lgn float64 // 经度 + Lat float64 // 纬度 + Loc *time.Location +} + +// 经纬度参数 +func LgnLatRead(deviceId string) (*LgnLatData, error) { + + ll := new(LgnLatData) + cmd, err := ReadCmd(deviceId, lgnLat, "读取经纬度参数") + if err != nil { + return nil, err + } + data := cmd.Data + lgn := float64(binary.LittleEndian.Uint32(data[0:4])) / 1000000 + lat := float64(binary.LittleEndian.Uint32(data[4:8])) / 1000000 + loc := float64(binary.LittleEndian.Uint32(data[8:len(data)])) / 1000000 + ll.Lgn = lgn + ll.Lat = lat + timezone, err := FloatToTimezone(loc) + if err != nil { + return nil, err + } + ll.Loc = timezone + + return ll, nil +} +func LgnLatWrite(deviceId string, data *LgnLatData) error { + // 将浮点型经纬度转换为字节编码(兼容原Read逻辑) + buf := make([]byte, 12) + + // 1. 处理经度(4字节) + lgnInt := uint32(math.Round(data.Lgn * 1000000)) + binary.LittleEndian.PutUint32(buf[0:4], lgnInt) + + // 2. 处理纬度(4字节) + latInt := uint32(math.Round(data.Lat * 1000000)) + binary.LittleEndian.PutUint32(buf[4:8], latInt) + + // 3. 处理时区(4字节) + // 从time.Location解析出UTC偏移小时数(含小数) + offsetHours, err := getTimezoneOffset(data.Loc) + if err != nil { + return err + } + offsetInt := uint32(math.Round(offsetHours * 1000000)) + binary.LittleEndian.PutUint32(buf[8:12], offsetInt) + writeResp, err := WriteCmd(deviceId, lgnLat, buf, "写入经纬度参数") + if err != nil { + return err + } + if !writeResp.Success { + return terror.New("设置失败") + } + return nil +} + +// 从time.Location中解析UTC偏移小时数(支持正负) +func getTimezoneOffset(loc *time.Location) (float64, error) { + // 任取一个时间点计算偏移(固定时区偏移不会变化) + t := time.Date(2023, 1, 1, 0, 0, 0, 0, loc) + _, offsetSec := t.Zone() + + // 转换为小时(含小数) + offsetHours := float64(offsetSec) / 3600.0 + if offsetHours < -12 || offsetHours > 14 { + return 0, fmt.Errorf("invalid timezone offset: %f", offsetHours) + } + return offsetHours, nil +} + +// FloatToTimezone 将float64时区值转换为 *time.Location +// 输入示例: 8.0 → UTC+8, -5.5 → UTC-5:30 +func FloatToTimezone(offset float64) (*time.Location, error) { + // 验证范围 (-12.0 到 +14.0) + if offset < -12.0 || offset > 14.0 { + return nil, fmt.Errorf("时区偏移量超出有效范围 (-12.0 到 +14.0)") + } + + // 分解小时和分钟 + hours := int(math.Floor(offset)) + minutes := int(math.Round((offset - float64(hours)) * 60)) + + // 处理负数情况 + if offset < 0 { + minutes = -minutes + } + + // 创建固定时区 + name := fmt.Sprintf("UTC%+03d:%02d", hours, minutes) + return time.FixedZone(name, hours*3600+minutes*60), nil +} diff --git a/internal/library/m9z/m9z_Manual.go b/internal/library/m9z/m9z_Manual.go new file mode 100644 index 0000000..b514c5d --- /dev/null +++ b/internal/library/m9z/m9z_Manual.go @@ -0,0 +1,257 @@ +package m9z + +// 手动控制指令 +import ( + "encoding/binary" + "fmt" + "github.com/towgo/towgo/errors/terror" + "strings" + "tgk-touch/internal/global" +) + +type ManualData struct { + Able bool `json:"disable"` //是否是手动控制状态 + BackupByte1 byte `json:"backup_byte1"` //备用字节下标1 + Loops []Loop `json:"loops"` //回路数据 + BackupByte4 byte `json:"backup_byte4"` //备用字节下标4 +} +type Loop struct { + Index int `json:"index"` // 0 D0 回路0 + Name string `json:"name"` // 0 #1 回路 + IsOpen bool `json:"is_open"` + Pwm int `json:"pwm"` // 功率百分比 +} + +func BuildManualData(enable, isOpen bool, pwm int) *ManualData { + md := &ManualData{ + Able: enable, + } + loops := make([]Loop, 10) + for i := range loops { + loops[i].Index = i + loops[i].IsOpen = isOpen + loops[i].Pwm = pwm + } + md.Loops = loops + return md +} +func (md *ManualData) Write(deviceId string) error { + return ManualWrite(deviceId, md) +} +func (md *ManualData) Display() string { + // 改用strings.Builder提升拼接性能(避免不可变字符串频繁创建临时对象) + var sb strings.Builder + // 遍历循环列表 + for _, l := range md.Loops { + // 将bool类型的IsOpen转为1(true)或0(false) + isOpenNum := 0 + if l.IsOpen { + isOpenNum = 1 + } + // 拼接格式:索引-开关状态(1/0)-调光值, + fmt.Fprintf(&sb, "%d-%d-%d,", l.Index, isOpenNum, l.Pwm) + } + // 转换为字符串并去除末尾多余的逗号(若字符串非空) + result := sb.String() + return fmt.Sprintf("%v-%s", md.Able, strings.TrimSuffix(result, ",")) + +} +func ManualRead(deviceId string) (*ManualData, error) { + + manualResult := new(ManualData) + readResp, err := ReadCmd(deviceId, manual, "读取手动控制参数") + if err != nil { + return nil, err + } + manualResult.Able = readResp.Data[0] == 01 + manualResult.BackupByte1 = readResp.Data[1] + manualResult.BackupByte4 = readResp.Data[4] + // 解析10路的继电器动作状态 + relayBits := BytesToUint16LE(readResp.Data[2:4]) + g.Log().Debugf("继电器状态(二进制): %016b ,% X", relayBits, readResp.Data[2:4]) // 补零显示16位 + + loops := make([]Loop, 10) + for i := 0; i < 10; i++ { + var loop Loop + bit := (relayBits >> i) & 0x01 // 取第i位的值 + loop.IsOpen = bit == 1 + loop.Index = i + loop.Name = fmt.Sprintf("#%+v", i+1) + loops[i] = loop + } + pwms := readResp.Data[5:len(readResp.Data)] + // 确定要处理的元素数量,取两者中的较小值 + processCount := len(pwms) + if len(loops) < processCount { + processCount = len(loops) + } + + // 只循环到有效的索引范围 + for index := 0; index < processCount; index++ { + loops[index].Pwm = hexToPercent(pwms[index]) + } + manualResult.Loops = loops + + return manualResult, err +} +func ManualWrite(deviceId string, manualData *ManualData) error { + // 1. 准备数据缓冲区(15字节:1+1+2+1+10) + data := make([]byte, 15) + + // 2. 填充手动控制状态(Byte0) + if manualData.Able { + data[0] = 0x01 + } else { + data[0] = 0x00 + } + + // 3. 填充备用字节1(Byte1) + data[1] = manualData.BackupByte1 + + // 4. 计算继电器控制位(Byte2-3) + var relayBits uint16 + for i, loop := range manualData.Loops { + if loop.IsOpen { + relayBits |= (1 << i) // 设置对应位为1 + } + } + binary.LittleEndian.PutUint16(data[2:4], relayBits) // 小端序写入 + + // 5. 填充备用字节4(Byte4) + data[4] = manualData.BackupByte4 + + // 6. 填充回路PWM百分比(Byte5-14) + for i, loop := range manualData.Loops { + if loop.Pwm < 0 { + loop.Pwm = 0 + } else if loop.Pwm > 100 { + loop.Pwm = 100 + } + data[5+i] = byte(loop.Pwm) // 直接转换百分比 + } + //EE 01 02 00 0F 01 00 64 64 00 3F 37 3C 3D 37 40 1C 37 40 1C 23 FF + //EE 01 02 00 0F 01 01 FF 03 00 32 32 32 32 32 32 32 32 32 32 3D FF + //g.Log().Debugf("manualData: % X", data) + // 8. 发送命令 + writeResp, err := WriteCmd(deviceId, manual, data, "写入手动模式") + if err != nil { + return err + } + if !writeResp.Success { + return terror.New("设置失败") + } + return nil +} + +// Enable 使能 +func (md *ManualData) Enable(deviceId string) error { + md.Able = true + return ManualWrite(deviceId, md) + +} + +// Disable 禁用 +func (md *ManualData) Disable(deviceId string) error { + md.Able = false + return ManualWrite(deviceId, md) +} + +// StartLoops 启动指定回路(如果index为空则启动所有) +func (md *ManualData) StartLoops(deviceId string, indexes ...int) error { + if !md.Able { + err := md.Enable(deviceId) + if err != nil { + return err + } + } + // 如果没有指定index,则操作所有回路 + if len(indexes) == 0 { + for i := range md.Loops { + md.Loops[i].IsOpen = true + } + } else { + // 否则只操作指定回路 + for _, idx := range indexes { + if idx >= 0 && idx < len(md.Loops) { + md.Loops[idx].IsOpen = true + } else { + return fmt.Errorf("无效的回路索引: %d", idx) + } + } + } + return ManualWrite(deviceId, md) +} + +// StopLoops 停止指定回路(如果index为空则停止所有) +func (md *ManualData) StopLoops(deviceId string, indexes ...int) error { + if !md.Able { + err := md.Enable(deviceId) + if err != nil { + return err + } + } + // 如果没有指定index,则操作所有回路 + if len(indexes) == 0 { + for i := range md.Loops { + md.Loops[i].IsOpen = false + } + } else { + // 否则只操作指定回路 + for _, idx := range indexes { + if idx >= 0 && idx < len(md.Loops) { + md.Loops[idx].IsOpen = false + } else { + return fmt.Errorf("无效的回路索引: %d", idx) + } + } + } + return ManualWrite(deviceId, md) +} + +// SetLoopPWM 设置指定回路的功率百分比 +// 参数: +// +// pwm: 功率百分比 (0-100) +// indexes: 回路索引(可变参数,不传则设置所有回路) +func (md *ManualData) SetLoopPWM(deviceId string, pwm int, indexes ...int) error { + if !md.Able { + err := md.Enable(deviceId) + if err != nil { + return err + } + } + // 校验PWM值范围 + if pwm < 0 || pwm > 100 { + return fmt.Errorf("功率百分比必须在0-100之间,当前值: %d", pwm) + } + + // 如果没有指定index,则操作所有回路 + if len(indexes) == 0 { + for i := range md.Loops { + md.Loops[i].Pwm = pwm + } + } else { + // 操作指定回路 + for _, idx := range indexes { + if idx >= 0 && idx < len(md.Loops) { + md.Loops[idx].Pwm = pwm + } else { + return fmt.Errorf("无效的回路索引: %d (可用范围: 0-%d)", + idx, len(md.Loops)-1) + } + } + } + + // 自动提交修改到设备 + return ManualWrite(deviceId, md) +} + +// GetLoopPWM 获取指定回路的功率百分比 +// 参数:index 回路索引(0-based) +// 返回值:(百分比, 错误信息) +func (md *ManualData) GetLoopPWM(index int) (int, error) { + if index < 0 || index >= len(md.Loops) { + return 0, fmt.Errorf("回路索引超出范围(可用:0-%d)", len(md.Loops)-1) + } + return md.Loops[index].Pwm, nil +} diff --git a/internal/library/m9z/m9z_Mode.go b/internal/library/m9z/m9z_Mode.go new file mode 100644 index 0000000..1f166fb --- /dev/null +++ b/internal/library/m9z/m9z_Mode.go @@ -0,0 +1,27 @@ +package m9z + +import ( + "github.com/towgo/towgo/errors/terror" + "log" +) + +// 编程模式0~2,默认为0模式。0x00~0x02 +// 对应模式01~模式03 +func ModeRead(deviceId string) (byte, error) { + readResp, err := ReadCmd(deviceId, mode, "读取模式") + if err != nil { + return 0, nil + } + log.Printf("readResp.Data % X", readResp.Data) + return readResp.Data[0], nil +} +func ModeWrite(deviceId string, m byte) error { + writeResp, err := WriteCmd(deviceId, mode, []byte{m}, "写入模式") + if err != nil { + return err + } + if !writeResp.Success { + return terror.New("设置失败") + } + return nil +} diff --git a/internal/library/m9z/m9z_RelayStatus.go b/internal/library/m9z/m9z_RelayStatus.go new file mode 100644 index 0000000..3a756e5 --- /dev/null +++ b/internal/library/m9z/m9z_RelayStatus.go @@ -0,0 +1,36 @@ +package m9z + +import ( + "fmt" +) + +// RelayStatusData 实际通断状态数据结构 +type RelayStatusData struct { + IsOpen bool // 0x01:闭合(true), 0x00:断开(false) +} + +// ParseRelayStatusResponse 解析实际通断状态响应 +// 响应格式示例: EE 99 01 00 02 01 00 D0 FF +// Data部分: 01 00 (长度为2,第一个字节为状态) +func ParseRelayStatusResponse(data []byte) (*RelayStatusData, error) { + if len(data) < 1 { + return nil, fmt.Errorf("invalid data length %d, expected at least 1 byte", len(data)) + } + + status := data[0] + return &RelayStatusData{ + IsOpen: status == 0x01, + }, nil +} + +// RelayStatusRead 读取实际通断情况 +// CMD = 0x19 +func RelayStatusRead(device string) (*RelayStatusData, error) { + // 调用 ReadCmd,指令码为 relayStatus (0x19),无索引(0x00) + readRep, err := ReadCmd(device, relayStatus, "读取实际通断情况", 0) + if err != nil { + return nil, err + } + + return ParseRelayStatusResponse(readRep.Data) +} diff --git a/internal/library/m9z/m9z_Restart.go b/internal/library/m9z/m9z_Restart.go new file mode 100644 index 0000000..10f3d95 --- /dev/null +++ b/internal/library/m9z/m9z_Restart.go @@ -0,0 +1,13 @@ +package m9z + +func Restart(deviceId string) error { + WriteCmd(deviceId, restart, nil, "重启设备") + /*writeResp, err := WriteCmd(deviceId, restart, nil) + if err != nil { + return err + } + if !writeResp.Success { + return terror.New("设置失败") + }*/ + return nil +} diff --git a/internal/library/m9z/m9z_Save.go b/internal/library/m9z/m9z_Save.go new file mode 100644 index 0000000..e6869a7 --- /dev/null +++ b/internal/library/m9z/m9z_Save.go @@ -0,0 +1,17 @@ +package m9z + +import ( + "github.com/towgo/towgo/errors/terror" +) + +func Save(deviceId string) error { + writeResp, err := WriteCmd(deviceId, save, nil, "保存设置") + if err != nil { + return err + } + + if !writeResp.Success { + return terror.New("设置失败") + } + return nil +} diff --git a/internal/library/m9z/m9z_SubLoopParameters.go b/internal/library/m9z/m9z_SubLoopParameters.go new file mode 100644 index 0000000..9eab20b --- /dev/null +++ b/internal/library/m9z/m9z_SubLoopParameters.go @@ -0,0 +1,123 @@ +package m9z + +import ( + "encoding/binary" + "fmt" + "github.com/gogf/gf/v2/util/gconv" +) + +// LoopCommonParameters 回路分控器公共参数结构体 (15个寄存器 = 30字节) +type LoopCommonParameters struct { + DCVoltage float32 // 直流电压 (0.1V单位) + DCCurrent float32 // 直流电流 (0.1A单位) + DimmingValue float32 // 调光幅度值 (0-100%) + AlarmStatus uint16 // 告警状态 (位图形式) 告警状态寄存器 0x01欠压 0x02过压 0x08电源故障 其中:欠压=短路故障,过压=开路故障,电源故障。 + AlarmCode string // 告警代码 + RelayStatus uint16 // 继电器状态 (位图形式) + LoopAddress string // 回路地址 + SubModuleCount uint16 // 子模块数量 + Reserved [14]byte // 备用字段 (7个寄存器=14字节) +} + +// LoopModuleParameters 回路子模块参数结构体 (13个寄存器 = 26字节) +type LoopModuleParameters struct { + OutputVoltage float32 // 模块输出电压值 (0.1V) + OutputCurrent float32 // 模块输出电流值 (0.01A) + CurrentPWMDuty float32 // 模块电流PWM占空比 (0-10000对应0-100%) + VoltagePWMDuty float32 // 模块电压PWM占空比 (0-10000对应0-100%) + DeviceAddress string // 模块的设备地址 + ModuleLoopAddress string // 模块的回路地址 + CurrentLimitPct float32 // 模块的限流百分数 (0-100对应0-100%) + OperatingMode uint16 // 模块手动/自动模式 (0=自动,1=手动) + ModuleVersion float32 // 模块的版本号 + RatedVoltageDuty uint16 // 额定配置电压PWM占空比 (0-10000对应0-100%) + RatedCurrentDuty uint16 // 额定配置电流PWM占空比 (0-10000对应0-100%) + RatedOutputVoltage uint16 // 额定配置输出电压值 (0.1V) + RatedOutputCurrent uint16 // 额定配置输出电流值 (0.01A) +} + +// LoopFullParameters 完整回路参数数据结构 +type LoopFullParameters struct { + LoopCommonParameters // 公共参数 (30字节) + Modules []LoopModuleParameters // 8个模块的参数 (208字节) +} + +// ParseLoopFullParameters 解析完整的238字节分控器数据 +func ParseLoopFullParameters(data []byte) (*LoopFullParameters, error) { + if len(data) < 238 { + return nil, fmt.Errorf("invalid data length %d, expected 238 bytes", len(data)) + } + + params := &LoopFullParameters{} + offset := 0 + + // 解析公共参数区 (30字节) + common := ¶ms.LoopCommonParameters + + common.DCVoltage = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.1 + offset += 2 + common.DCCurrent = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.1 + offset += 2 + common.DimmingValue = float32(binary.LittleEndian.Uint16(data[offset:])) * 10 * 0.01 + offset += 2 + //log.Debugf("AlarmStatus [% X]", data) + common.AlarmStatus = uint16(data[offset:][0]) + common.AlarmCode = fmt.Sprintf("% X", data[offset:offset+2]) + offset += 2 + common.RelayStatus = binary.LittleEndian.Uint16(data[offset:]) + offset += 2 + if binary.LittleEndian.Uint16(data[offset:]) == 0 { + return nil, nil + } + common.LoopAddress = fmt.Sprintf("0x%x", binary.LittleEndian.Uint16(data[offset:])) + + offset += 2 + common.SubModuleCount = binary.LittleEndian.Uint16(data[offset:]) + offset += 2 + copy(common.Reserved[:], data[offset:offset+16]) + offset += 16 + params.Modules = make([]LoopModuleParameters, common.SubModuleCount) + // 解析8个模块 (每个26字节) + for i := 0; i < int(common.SubModuleCount); i++ { + module := ¶ms.Modules[i] + module.OutputVoltage = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.1 + offset += 2 + module.OutputCurrent = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.1 + offset += 2 + module.CurrentPWMDuty = float32(binary.LittleEndian.Uint16(data[offset:])) * 10 * 0.01 + offset += 2 + module.VoltagePWMDuty = float32(binary.LittleEndian.Uint16(data[offset:])) * 10 * 0.01 + offset += 2 + module.DeviceAddress = fmt.Sprintf("0x%x", binary.LittleEndian.Uint16(data[offset:])) + offset += 2 + module.ModuleLoopAddress = fmt.Sprintf("0x%x", binary.LittleEndian.Uint16(data[offset:])) + offset += 2 + module.CurrentLimitPct = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.1 + offset += 2 + module.OperatingMode = binary.LittleEndian.Uint16(data[offset:]) + offset += 2 + version := fmt.Sprintf("%x", binary.LittleEndian.Uint16(data[offset:])) + floatVersion := gconv.Float32(version) + module.ModuleVersion = floatVersion * 0.1 + offset += 2 + module.RatedVoltageDuty = binary.LittleEndian.Uint16(data[offset:]) + offset += 2 + module.RatedCurrentDuty = binary.LittleEndian.Uint16(data[offset:]) + offset += 2 + module.RatedOutputVoltage = binary.LittleEndian.Uint16(data[offset:]) + offset += 2 + module.RatedOutputCurrent = binary.LittleEndian.Uint16(data[offset:]) + offset += 2 + } + + return params, nil +} + +func SubLoopParametersRead(device string, idx uint) (*LoopFullParameters, error) { + readRep, err := ReadCmd(device, subLoopParameters, "读取回路分控器的参数", idx) + if err != nil { + return nil, err + } + + return ParseLoopFullParameters(readRep.Data) +} diff --git a/internal/library/m9z/m9z_SunRiseSet.go b/internal/library/m9z/m9z_SunRiseSet.go new file mode 100644 index 0000000..6732902 --- /dev/null +++ b/internal/library/m9z/m9z_SunRiseSet.go @@ -0,0 +1,69 @@ +package m9z + +import ( + "fmt" + "github.com/towgo/towgo/errors/terror" + "log" +) + +// 日出日落时间偏差调节 +// SunRiseSet 日出日落时间偏差调节(单位:分钟) +type SunRiseSet struct { + Rise int `json:"rise"` // 日出偏差(-60~60分钟) + Set int `json:"set"` // 日落偏差(-60~60分钟) +} + +func SunRiseSetRead(deviceId string) (*SunRiseSet, error) { + cmd, err := ReadCmd(deviceId, sunRiseSet, "读取日出日落时间偏差") + if err != nil { + return nil, err + } + data := cmd.Data + srs := new(SunRiseSet) + log.Printf("日出日落偏差% X", data) + if int(data[0]) > 60 { + srs.Rise = -(256 - int(data[0])) + } else { + srs.Rise = int(data[0]) + } + if int(data[1]) > 60 { + srs.Set = -(256 - int(data[1])) + } else { + srs.Set = int(data[1]) + } + log.Println(srs.Rise, srs.Set) + return srs, nil +} +func (srs *SunRiseSet) Write(deviceId string) error { + return SunRiseSetWrite(deviceId, srs) +} + +// SunRiseSetWrite 写入日出日落时间偏差设置 +func SunRiseSetWrite(deviceId string, srs *SunRiseSet) error { + // 1. 参数校验 + if srs.Rise < -60 || srs.Rise > 60 { + return fmt.Errorf("日出偏差值超出范围(-60~60分钟),当前值:%d", srs.Rise) + } + if srs.Set < -60 || srs.Set > 60 { + return fmt.Errorf("日落偏差值超出范围(-60~60分钟),当前值:%d", srs.Set) + } + + // 2. 准备写入数据(2字节) + data := []byte{ + byte(srs.Rise), // 日出偏差 + byte(srs.Set), // 日落偏差 + } + + // 4. 发送命令 + resp, err := WriteCmd(deviceId, sunRiseSet, data, "写入日出日落时间偏差") + if err != nil { + return fmt.Errorf("设备通信失败: %v", err) + } + + // 5. 验证设备响应 + if !resp.Success { + return terror.New("设备返回写入失败") + } + + return nil +} diff --git a/internal/library/m9z/m9z_SunRiseTime.go b/internal/library/m9z/m9z_SunRiseTime.go new file mode 100644 index 0000000..9131d10 --- /dev/null +++ b/internal/library/m9z/m9z_SunRiseTime.go @@ -0,0 +1,43 @@ +package m9z + +import ( + "encoding/binary" + "fmt" + "time" +) + +type SunTimes struct { + Sunrise time.Time // 日出时间(转换为当日时间对象) + Sunset time.Time // 日落时间 + RawSunrise int16 // 原始日出分钟数(从午夜开始的分钟偏移) + RawSunset int16 // 原始日落分钟数 +} + +func SunRiseTimeRead(deviceId string) (*SunTimes, error) { + cmd, err := ReadCmd(deviceId, sunRiseTime, "读取日出日落时间") + if err != nil { + return nil, err + } + data := cmd.Data + + // 数据长度校验(至少需要4字节) + if len(data) < 4 { + return nil, fmt.Errorf("invalid data length: %d, expect at least 4 bytes", len(data)) + } + + st := &SunTimes{} + + // 1. 解析日出时间(小端序int16) + st.RawSunrise = int16(binary.LittleEndian.Uint16(data[0:2])) + + // 2. 解析日落时间(小端序int16) + st.RawSunset = int16(binary.LittleEndian.Uint16(data[2:4])) + + // 转换为当日时间(假设基于UTC的当天) + toDayFmt := time.Now().Format(time.DateOnly) + location, _ := time.LoadLocation("Asia/Shanghai") + baseTime, _ := time.ParseInLocation(time.DateOnly, toDayFmt, location) + st.Sunrise = baseTime.Add(time.Duration(st.RawSunrise) * time.Minute) + st.Sunset = baseTime.Add(time.Duration(st.RawSunset) * time.Minute) + return st, nil +} diff --git a/internal/library/m9z/m9z_Task.go b/internal/library/m9z/m9z_Task.go new file mode 100644 index 0000000..49f1659 --- /dev/null +++ b/internal/library/m9z/m9z_Task.go @@ -0,0 +1,139 @@ +package m9z + +import ( + "encoding/binary" + "fmt" + "github.com/towgo/towgo/errors/terror" + "tgk-touch/internal/global" +) + +// 定义任务指令结构 +type TaskInstruction struct { + Type uint32 `json:"type"` // 指令类型 (ISA_CMP, ISA_GOTO, etc.) + Param uint32 `json:"param"` + Value uint32 `json:"value"` // 指令参数 +} + +// 定义任务结构 +type TaskProgram struct { + Index uint8 `json:"index"` // 任务索引 (0~2) + Size uint8 `json:"size"` // 指令数量 + Instructions []TaskInstruction `json:"instructions"` // 指令列表 +} + +// StartTaskRead 读取开始任务 (优化指令 CMD=0x13) +func StartTaskRead(deviceId string, idx uint8) (*TaskProgram, error) { + return taskRead(deviceId, startTask, idx, "读取开始任务") +} + +// MiddleTaskRead 读取中间任务 (优化指令 CMD=0x14) +func MiddleTaskRead(deviceId string, idx uint8) (*TaskProgram, error) { + // 实现与StartTaskRead相同,仅CMD不同 + return taskRead(deviceId, middleTask, idx, "读取中间任务") +} + +// StopTaskRead 读取结束任务 (优化指令 CMD=0x15) +func StopTaskRead(deviceId string, idx uint8) (*TaskProgram, error) { + // 实现与StartTaskRead相同,仅CMD不同 + return taskRead(deviceId, stopTask, idx, "读取结束任务") +} + +// 通用任务读取函数 +func taskRead(deviceId string, cmd M9zCtrl, idx uint8, detail string) (*TaskProgram, error) { + readResp, err := ReadCmd(deviceId, cmd, detail, uint(idx)) + if err != nil { + return nil, err + } + + data := readResp.Data + + // 解析逻辑与StartTaskRead相同 + if idx < 0 || idx > 2 { + idx = 0 + } + task := &TaskProgram{Index: idx} + if len(data) < 1 { + return nil, fmt.Errorf("invalid task data length") + } + + task.Size = data[0] + task.Instructions = make([]TaskInstruction, 0, task.Size) + + for i := 1; i+3 < len(data) && len(task.Instructions) < int(task.Size); i += 4 { + instruction := binary.LittleEndian.Uint32(data[i : i+4]) + g.Log().Debugf("% X", instruction) + insType := uint32(instruction >> 28) + ins := TaskInstruction{Type: insType} + + // 根据指令类型解析参数 + switch insType { + case 0x01: // ISA_CMP + // 参数: 源类型 + 比较值 + ins.Param = (instruction >> 24) & 0x0F + ins.Value = instruction & 0x0000FFFF + case 0x02: // ISA_GOTO + // 参数: 跳转类型 + 跳转地址 + 最大次数 + 索引值 + ins.Param = (instruction >> 24) & 0x0F + ins.Value = ((instruction >> 16) & 0xFF) | ((instruction >> 8) & 0xFF) | (instruction & 0xFF) + case 0x03: // ISA_DLY + // 参数: 延迟单位 + 延迟值 + ins.Param = (instruction >> 24) & 0x0F + ins.Value = instruction & 0x00FFFFFF + case 0x04: // ISA_OUTPUT + // 参数: 输出类型 + 输出值 + ins.Param = (instruction >> 24) & 0x0F + ins.Value = instruction & 0x00FFFFFF // 输出值 + + default: + // 未知指令类型 + ins.Param = instruction + } + + task.Instructions = append(task.Instructions, ins) + } + + return task, nil +} + +// StartTaskWrite 写入开始任务 (优化指令 CMD=0x13) +func StartTaskWrite(deviceId string, task *TaskProgram) error { + return taskWrite(deviceId, startTask, task, "写入开始任务") +} + +// MiddleTaskWrite 写入中间任务 (优化指令 CMD=0x14) +func MiddleTaskWrite(deviceId string, task *TaskProgram) error { + return taskWrite(deviceId, middleTask, task, "写入中间任务") +} + +// StopTaskWrite 写入结束任务 (优化指令 CMD=0x15) +func StopTaskWrite(deviceId string, task *TaskProgram) error { + return taskWrite(deviceId, stopTask, task, "写入结束任务") +} + +// 通用任务写入函数 +func taskWrite(deviceId string, cmd M9zCtrl, task *TaskProgram, detail string) error { + if task.Size > 23 { + return fmt.Errorf("任务数量 %+v 太多,超过了上限,请删除一些后重试}", task.Size) + } + // 构造数据缓冲区 (1字节大小 + 每个指令4字节) + buf := make([]byte, 1+4*len(task.Instructions)) + buf[0] = byte(len(task.Instructions)) + + offset := 1 + for _, ins := range task.Instructions { + // 组合指令 (类型在高4位) + instruction := ins.Type | ins.Param | ins.Value + // 写入小端序 + binary.LittleEndian.PutUint32(buf[offset:offset+4], instruction) + offset += 4 + } + writeResp, err := WriteCmd(deviceId, M9zCtrl(cmd), buf, detail, task.Index) + if err != nil { + return err + } + if !writeResp.Success { + return terror.New("设置失败") + } + // 发送写入命令 + return nil +} diff --git a/internal/library/m9z/m9z_Task_Plugin.go b/internal/library/m9z/m9z_Task_Plugin.go new file mode 100644 index 0000000..57c7219 --- /dev/null +++ b/internal/library/m9z/m9z_Task_Plugin.go @@ -0,0 +1,674 @@ +package m9z // 插件 +import ( + "encoding/binary" + "fmt" + "github.com/towgo/towgo/errors/terror" + "sort" + "tgk-touch/internal/global" + "time" +) + +var ( + DefaultStartTask ControlTask = ControlTask{ + TimeType: 0, + Loops: []TaskLoop{ + { + 0, true, 70, + }, + { + 1, true, 70, + }, + { + 2, true, 70, + }, + { + 3, true, 70, + }, + { + 4, true, 70, + }, + { + 5, true, 70, + }, + { + 6, true, 70, + }, + { + 7, true, 70, + }, + { + 8, true, 70, + }, + { + 9, true, 70, + }, + }, + } + DefaultMiddleTask MiddleControlTask = MiddleControlTask{ + []ControlTask{ + ControlTask{ + + TimeType: 0, + Loops: []TaskLoop{ + { + 0, true, 70, + }, + { + 1, true, 70, + }, + { + 2, true, 70, + }, + { + 3, true, 70, + }, + { + 4, true, 70, + }, + { + 5, true, 70, + }, + { + 6, true, 70, + }, + { + 7, true, 70, + }, + { + 8, true, 70, + }, + { + 9, true, 70, + }, + }, + }, + }, + } + DefaultStopTask ControlTask = ControlTask{ + + TimeType: 0, + Loops: []TaskLoop{ + { + 0, false, 70, + }, + { + 1, false, 70, + }, + { + 2, false, 70, + }, + { + 3, false, 70, + }, + { + 4, false, 70, + }, + { + 5, false, 70, + }, + { + 6, false, 70, + }, + { + 7, false, 70, + }, + { + 8, false, 70, + }, + { + 9, false, 70, + }, + }, + } +) + +func init() { + parse, _ := time.ParseInLocation("2006-01-02 15:04:05", "2025-06-05 19:00:00", time.Local) + + DefaultStartTask.ExecTime = parse + for i, c := range DefaultMiddleTask.ControlTaskList { + p, _ := time.ParseInLocation("2006-01-02 15:04:05", "2025-06-05 20:00:00", time.Local) + c.ExecTime = p + DefaultMiddleTask.ControlTaskList[i] = c + } + parse, _ = time.ParseInLocation("2006-01-02 15:04:05", "2025-06-05 07:00:00", time.Local) + DefaultStartTask.ExecTime = parse +} + +func IsTaskInitRead(deviceId string) (bool, error) { + read, err := ManualRead(deviceId) + if err != nil { + return false, err + } + return read.BackupByte1 == 1, nil +} +func SetIsTaskInitRead(deviceId string, b bool) error { + read, err := ManualRead(deviceId) + if err != nil { + return err + } + if b { + read.BackupByte1 = 1 + } else { + read.BackupByte1 = 0 + } + return ManualWrite(deviceId, read) +} + +type TaskLoop struct { + Index uint `json:"index"` // 0 D0 回路0 + TurnOn bool `json:"turnOn"` + Pwm uint8 `json:"pwm"` // 功率百分比 +} + +// 控制任务配置 +type ControlTask struct { + ExecTime time.Time `json:"execTime"` // "2006-01-02 15:04:05" 格式时间 + TimeType int `json:"timeType"` // 0 定时 1经纬 + Loops []TaskLoop `json:"loops"` +} +type MiddleControlTask struct { + ControlTaskList []ControlTask `json:"controlTaskList"` +} + +func ControlTaskToTaskProgram(task ControlTask, idx uint8) (*TaskProgram, error) { + if uint8(0) > idx || idx > uint8(2) { + idx = 0 + } + // 1. 创建任务程序对象 + program := &TaskProgram{ + Index: idx, + } + + // 2. 解析执行时间 + eTime := task.ExecTime + g.Log().Debugf("task:%+v\ntask.ExecTime %+v , eTime:%+v,hour:%d,min:%d", task, task.ExecTime, eTime, eTime.Hour(), eTime.Minute()) + minValue := uint32(eTime.Hour()*60 + eTime.Minute()) + var cmpInstruction TaskInstruction + // 3. 添加时间比较指令 + if task.TimeType == 0 { + cmpInstruction = TaskInstruction{ + Type: 0x01 << 28, // CMP指令类型 + Param: 0x01 << 24, + Value: minValue, + } + } else { + cmpInstruction = TaskInstruction{ + Type: 0x01 << 28, + Param: 0x02 << 24, + Value: minValue, + } + } + + program.Instructions = append(program.Instructions, cmpInstruction) + + // 开灯任务需要两条指令: + // 1. 开启所有回路继电器 + var openLoopValue uint32 + var closeLoopValue uint32 + for _, loop := range task.Loops { + if loop.TurnOn { + openLoopValue |= (0x1 << loop.Index) + } else { + closeLoopValue |= (0x1 << loop.Index) + } + if loop.Pwm > 100 { + loop.Pwm = 100 + } + loopIndex := (0x1 << loop.Index) + loopIndex |= (0x1 << loop.Index) + // 修复类型不匹配问题 + pwmValue := (uint32(loopIndex) << 12) | uint32(loop.Pwm) + + pwmInstruction := TaskInstruction{ + Type: 0x4 << 28, // OUTPUT指令 + Param: 0x03 << 24, + Value: pwmValue, + } + program.Instructions = append(program.Instructions, pwmInstruction) + } + if openLoopValue != 0 { + openLoopValue = (openLoopValue << 12) | 0x01 + } + if closeLoopValue != 0 { + closeLoopValue = (closeLoopValue << 12) | 0x00 + } + + openLoopIns := TaskInstruction{ + Type: 0x04 << 28, // OUTPUT指令 + Param: 0x01 << 24, + Value: openLoopValue, + } + closeLoopIns := TaskInstruction{ + Type: 0x04 << 28, // OUTPUT指令 + Param: 0x01 << 24, + Value: closeLoopValue, + } + + program.Instructions = append(program.Instructions, openLoopIns, closeLoopIns) + // 2. 设置PWM亮度 + // 5. 设置任务大小(现在有3条指令) + program.Size = uint8(len(program.Instructions)) + + return program, nil +} + +func TaskProgramToControlTask(program *TaskProgram) (*ControlTask, error) { + task := &ControlTask{ + TimeType: 0, // 默认定时类型 + // 初始化所有10个回路(0-9) + Loops: make([]TaskLoop, 10), + } + + // 初始化所有回路为关闭状态 + for i := range task.Loops { + task.Loops[i] = TaskLoop{ + Index: uint(i), + TurnOn: false, + Pwm: 0, + } + } + + foundTimeInstruction := false + + // 解析所有指令 + for _, ins := range program.Instructions { + g.Log().Debugf("Found instruction: %+v,type %+v isTime %+v", ins, ins.Type, ins.Type == 0x01) + switch ins.Type { + case 0x01: // 时间命令 + foundTimeInstruction = true + minutes := ins.Value + hour := minutes / 60 + minute := minutes % 60 + parse, err := time.ParseInLocation("2006-01-02 15:04:05", fmt.Sprintf("%s %02d:%02d:00", time.Now().Format(time.DateOnly), hour, minute), time.Local) + if err != nil { + return nil, terror.Wrapf(err, "failed to parse time") + } + task.ExecTime = parse + + // 处理时间类型 + switch ins.Param { + case 0x01: + task.TimeType = 0 // 定时 + case 0x02: + task.TimeType = 1 // 经纬 + } + + case 0x04: // OUTPUT指令 + paramType := ins.Param + switch paramType { + case 0x01: // 继电器状态命令 + relayValue := ins.Value + // 判断是开还是关 + isOpen := (relayValue & 1) == 1 + // 获取开启掩码(高12位) + onMask := (relayValue >> 12) & 0xFFF + + // 颠倒位顺序 - 低位对应低索引回路 + reversedOnMask := uint32(0) + for i := 0; i < 12; i++ { + originalBit := (onMask >> i) & 0x1 + reversedOnMask |= (originalBit << (11 - i)) + } + + str := fmt.Sprintf("%12b", reversedOnMask) + for i := 0; i < len(str)-2; i++ { + if str[i] == '1' { + if isOpen { + task.Loops[i].TurnOn = true + g.Log().Debugf("isOpen %+v ,loop %+v ,on %q ,t :%+v", isOpen, i, str[i], true) + + } else { + task.Loops[i].TurnOn = false + g.Log().Debugf("isOpen %+v ,loop %+v ,on %q ,t :%+v", isOpen, i, str[i], false) + + } + } + } + + case 0x03: // PWM指令 + pwmValue := ins.Value + // 高12位:通道掩码,低12位:PWM值 + channelMask := (pwmValue >> 12) & 0xFFF + pwmVal := uint8(pwmValue & 0xFF) // 取低8位作为PWM值 + + // 处理每个回路的PWM值 + for i := 0; i < 12; i++ { + if i < len(task.Loops) && (channelMask&(1< 0 { + activeLoops = append(activeLoops, loop) + } + } + task.Loops = activeLoops*/ + + return task, nil +} + +func MiddleControlTaskToTaskProgram(middle MiddleControlTask, idx uint8) (*TaskProgram, error) { + sort.Slice(middle.ControlTaskList, func(i, j int) bool { + t1 := middle.ControlTaskList[i].ExecTime + t2 := middle.ControlTaskList[i].ExecTime + return t1.Before(t2) + }) + if idx < 0 || idx > 2 { + idx = 0 + } + program := &TaskProgram{ + Index: idx, + } + fmt.Printf("middleControlTaskToTaskProgram middle:%+v\n", middle) + + for _, task := range middle.ControlTaskList { + + // 2. 解析执行时间 + eTime := task.ExecTime + fmt.Printf("task:%+v\ntask.ExecTime %+v , eTime:%+v,hour:%d,min:%d\n", task, task.ExecTime, eTime, eTime.Hour(), eTime.Minute()) + + minValue := uint32(eTime.Hour()*60 + eTime.Minute()) + var cmpInstruction TaskInstruction + // 3. 添加时间比较指令 + if task.TimeType == 0 { + cmpInstruction = TaskInstruction{ + Type: 0x01 << 28, // CMP指令类型 + Param: 0x5 << 24, + Value: minValue, + } + } else { + cmpInstruction = TaskInstruction{ + Type: 0x01 << 28, + Param: 0x04 << 24, + Value: minValue, + } + } + + program.Instructions = append(program.Instructions, cmpInstruction) + + // 开灯任务需要两条指令: + // 1. 开启所有回路继电器 + var openLoopValue uint32 + var closeLoopValue uint32 + // 2. PWM指令(针对每个回路) + pwmMap := make(map[uint8][]uint) // 按PWM值分组 + for _, loop := range task.Loops { + if loop.Pwm > 0 { + pwmMap[loop.Pwm] = append(pwmMap[loop.Pwm], loop.Index) + } + } + // 为每个PWM值创建指令 + for pwmVal, indices := range pwmMap { + channelMask := uint32(0) + for _, idx2 := range indices { + if idx2 < 12 { + channelMask |= (1 << idx2) + } + } + + pwmInstruction := TaskInstruction{ + Type: 0x4 << 28, // OUTPUT指令 + Param: 0x03 << 24, // PWM类型 + Value: (channelMask << 12) | uint32(pwmVal), + } + program.Instructions = append(program.Instructions, pwmInstruction) + } + for _, loop := range task.Loops { + if loop.TurnOn { + openLoopValue |= (0x1 << loop.Index) + } else { + closeLoopValue |= (0x1 << loop.Index) + } + } + if openLoopValue != 0 { + openLoopValue = (openLoopValue << 12) | 0x01 + } + if closeLoopValue != 0 { + closeLoopValue = (closeLoopValue << 12) | 0x00 + } + openLoopIns := TaskInstruction{ + Type: 0x04 << 28, // OUTPUT指令 + Param: 0x01 << 24, + Value: openLoopValue, + } + if openLoopValue != 0 { + program.Instructions = append(program.Instructions, openLoopIns) + + } + closeLoopIns := TaskInstruction{ + Type: 0x04 << 28, // OUTPUT指令 + Param: 0x01 << 24, + Value: closeLoopValue, + } + + if closeLoopValue != 0 { + program.Instructions = append(program.Instructions, closeLoopIns) + } + + } + // 中间任务需要循环 + loopIns := TaskInstruction{ + Type: 0x2 << 28, + Param: ((0x1 /*跳转使能*/) & 0xF) << 24, + Value: (((0 /*跳转到第几步*/) & 0xFF) << 16) | (((0 /*最大次数*/) & 0xFF) << 8), + } + program.Instructions = append(program.Instructions, loopIns) + program.Size = uint8(len(program.Instructions)) + return program, nil +} + +func TaskProgramToMiddleControlTask(program *TaskProgram) (*MiddleControlTask, error) { + middle := &MiddleControlTask{ + ControlTaskList: make([]ControlTask, 0), + } + + // 存储当前正在解析的任务 + var currentTask *ControlTask + taskIndex := 0 + + for i, ins := range program.Instructions { + insType := ins.Type + paramType := ins.Param + + g.Log().Debugf("解析指令 %d: Type=0x%X, Param=0x%X, Value=0x%08X", i, insType, paramType, ins.Value) + switch insType { + case 0x01: // 时间比较指令 + // 完成前一个任务(如果有) + if currentTask != nil { + middle.ControlTaskList = append(middle.ControlTaskList, *currentTask) + taskIndex++ + } + + // 创建新任务 + currentTask = &ControlTask{ + Loops: make([]TaskLoop, 10), // 10个回路的默认配置 + } + + // 初始化所有回路状态 + for j := range currentTask.Loops { + currentTask.Loops[j] = TaskLoop{ + Index: uint(j), + TurnOn: false, + Pwm: 0, + } + } + + // 解析时间 + minutes := ins.Value + hour := minutes / 60 + minute := minutes % 60 + parse, err := time.ParseInLocation("2006-01-02 15:04", fmt.Sprintf("%s %02d:%02d", time.Now().Format(time.DateOnly), hour, minute), time.Local) + + if err != nil { + return nil, terror.Wrap(err, "parse time error") + } + currentTask.ExecTime = parse + + // 解析时间类型 + switch paramType { + case 0x5: + currentTask.TimeType = 0 // 定时 + case 0x4: + currentTask.TimeType = 1 // 经纬 + default: + g.Log().Warnf("未知的时间类型: 0x%X", paramType) + currentTask.TimeType = 0 + } + + g.Log().Debugf("时间指令: %s, 时间类型: %d ,分钟数:%d", currentTask.ExecTime, currentTask.TimeType, minutes) + case 0x04: // OUTPUT指令 + if currentTask == nil { + g.Log().Warnf("在时间指令前发现OUTPUT指令,忽略") + continue + } + + switch paramType { + case 0x01: // 继电器状态命令 + + value := ins.Value + // 判断是开还是关 + isOpen := (value & 1) == 1 + onMask := (value >> 12) & 0xFFF + + // 输出原始掩码值(二进制12位格式) + g.Log().Debugf("开关命令%+v", isOpen) + g.Log().Debugf("原始继电器掩码: %012b (onMask = 0x%03X)", onMask, onMask) + + // 颠倒位顺序 - 低位对应低索引回路 + reversedOnMask := uint32(0) + for j := 0; j < 12; j++ { + originalBit := (onMask >> j) & 0x1 + reversedOnMask |= (originalBit << (11 - j)) + } + + // 输出反转后的掩码值 + str := fmt.Sprintf("%012b", reversedOnMask) + g.Log().Debugf("反转后继电器掩码: %s (reversedOnMask = 0x%03X)", str, reversedOnMask) + + // 逐个输出每个回路的位状态 + g.Log().Debug("按位解析回路状态:") + for j := 0; j < len(str)-2; j++ { + if j >= len(currentTask.Loops) { + break + } + + if str[j] == '1' { + if isOpen { + currentTask.Loops[j].TurnOn = str[j] == '1' && isOpen + g.Log().Debugf("loop %+v ,on %q ,t :%+v", j, str[j], true) + } else { + currentTask.Loops[j].TurnOn = false + g.Log().Debugf("loop %+v ,on %q ,t :%+v", j, str[j], false) + + } + } + + } + + case 0x03: // PWM指令 + value := ins.Value + channelMask := (value >> 12) & 0xFFF + pwmVal := uint8(value & 0xFF) + + // 输出原始掩码值 + g.Log().Debugf("原始PWM掩码: %012b (channelMask = 0x%03X), PWM值: %d", channelMask, channelMask, pwmVal) + + // 颠倒位顺序 - 低位对应低索引回路 + reversedChannelMask := uint32(0) + for j := 0; j < 12; j++ { + originalBit := (channelMask >> j) & 0x1 + reversedChannelMask |= (originalBit << (11 - j)) + } + + // 输出反转后的掩码值 + str := fmt.Sprintf("%012b", reversedChannelMask) + g.Log().Debugf("反转后PWM掩码: %s (reversedChannelMask = 0x%03X)", str, reversedChannelMask) + + // 设置每个回路的PWM值 + for j := 0; j < len(str); j++ { + if j >= len(currentTask.Loops) { + break + } + + if str[j] == '1' { + g.Log().Debugf(" 设置回路 %d PWM值: %d%%", j, pwmVal) + currentTask.Loops[j].Pwm = pwmVal + } + } + } + + case 0x02: // 循环指令 + g.Log().Debug("检测到循环指令,跳过") + + default: + g.Log().Warnf("未知的指令类型: 0x%X", insType) + } + } + + // 添加最后一个任务(如果有) + if currentTask != nil { + middle.ControlTaskList = append(middle.ControlTaskList, *currentTask) + } + + g.Log().Debugf("成功解析 %d 个控制任务", len(middle.ControlTaskList)) + + return middle, nil +} + +// 写入开始任务到设备 +func StartTaskWriteByTaskCfg(deviceId string, idx uint8, task ControlTask) error { + // 1. 将控制任务转换为任务程序 + tp, err := ControlTaskToTaskProgram(task, idx) + if err != nil { + return err + } + + // 2. 使用CMD 0x13写入开始任务 + return StartTaskWrite(deviceId, tp) +} + +// 写入任务程序到设备 +func StartTaskWrite2(deviceId string, program *TaskProgram) error { + // 1. 构造数据缓冲区 (1字节大小 + 每个指令4字节) + buf := make([]byte, 1+4*len(program.Instructions)) + buf[0] = program.Size + + offset := 1 + for _, ins := range program.Instructions { + // 2. 组合指令 (类型在高4位) + instruction := uint32(ins.Type) << 28 + + /*// 3. 根据指令类型组合参数 + switch len(ins.Params) { + case 1: + instruction |= ins.Param & 0x0FFFFFFF + case 2: + instruction |= (ins.Param & 0x0F) << 24 + instruction |= ins.Value & 0x00FFFFFF + case 3: + instruction |= (ins.Param & 0x0F) << 24 + instruction |= (ins.Value & 0xFFFF) << 8 + instruction |= ins.Params[2] & 0xFF + }*/ + + // 4. 写入小端序 + binary.LittleEndian.PutUint32(buf[offset:offset+4], instruction) + offset += 4 + } + + // 5. 发送写入命令 + + return StartTaskWrite(deviceId, program) +} diff --git a/internal/library/m9z/m9z_Version.go b/internal/library/m9z/m9z_Version.go new file mode 100644 index 0000000..c6be0f7 --- /dev/null +++ b/internal/library/m9z/m9z_Version.go @@ -0,0 +1,43 @@ +package m9z + +import ( + "fmt" +) + +// 版本信息结构体(类似SunRiseSet的设计) +type VersionInfo struct { + Version string // 版本号,如"v4.5" + RawByte byte // 原始字节值,如0x45 +} + +// ReadVersion 读取设备固件版本号(风格与SunRiseSetRead保持一致) +func ReadVersion(deviceId string) (*VersionInfo, error) { + var versionReadCmd = []byte{0xEE, 0x0E, 0x01, 0x00, 0x06, 0x00, 0x50, 0x01, 0x01, 0x08, 0x20, 0x00, 0xC2, 0xFF} + cmdRespData, err := pushCommand(deviceId, versionReadCmd, "读取设备版本号") + if err != nil { + return nil, err + } + frame, err := ParseFrame(cmdRespData) + if err != nil { + return nil, err + } + response := frame.(*ReadResponse) + data := response.Data + + // 校验数据长度(版本号应为1字节,参考协议中"数据项"格式) + if len(data) < 1 { + return nil, fmt.Errorf("版本数据长度不足,期望至少1字节,实际%v字节", len(data)) + } + + // 解析版本字节(核心逻辑:高4位为主版本,低4位为次版本) + versionByte := data[23] + mainVersion := (versionByte >> 4) & 0x0F // 提取高4位 + subVersion := versionByte & 0x0F // 提取低4位 + + // 构造版本信息结构体 + versionInfo := &VersionInfo{ + Version: fmt.Sprintf("v%d.%d", mainVersion, subVersion), + RawByte: versionByte, + } + return versionInfo, nil +} diff --git a/internal/library/m9z/m9z_cmdOperate.go b/internal/library/m9z/m9z_cmdOperate.go new file mode 100644 index 0000000..310e5ce --- /dev/null +++ b/internal/library/m9z/m9z_cmdOperate.go @@ -0,0 +1,322 @@ +package m9z + +import ( + "fmt" + "github.com/towgo/towgo/errors/terror" + "tgk-touch/internal/global" + "tgk-touch/internal/module/maincontrollerClient" +) + +const ( + FrameHeader = 0xEE + FrameFooter = 0xFF + CmdRead = 0x01 + CmdWrite = 0x02 + ResponseFlag = 0x80 // D7位掩码 + MaxPacketSize = 256 +) + +// 读取命令结构 +type ReadCommand struct { + Instruction byte + Index byte +} + +// 写入命令结构 +type WriteCommand struct { + Instruction byte + Index byte + Data []byte + IsUpdate bool +} + +// 读取响应结构 +type ReadResponse struct { + Instruction byte + Index byte + Data []byte +} + +// 写入响应结构 +type WriteResponse struct { + Instruction byte + Index byte + Success bool +} + +// 校验和计算 +func calCheckSum(data []byte) byte { + sum := byte(0x33) + for _, b := range data { + sum += b + } + return sum +} + +// 构建读取命令帧 +func BuildReadCommand(cmd ReadCommand) []byte { + frame := []byte{FrameHeader} + payload := []byte{cmd.Instruction, CmdRead, cmd.Index} + frame = append(frame, payload...) + frame = append(frame, calCheckSum(payload)) + frame = append(frame, FrameFooter) + return frame +} + +// 构建写入命令帧 +func BuildWriteCommand(cmd WriteCommand) []byte { + frame := []byte{FrameHeader} + payload := []byte{cmd.Instruction, CmdWrite, cmd.Index} + if len(cmd.Data) > 0 && !cmd.IsUpdate { + payload = append(payload, byte(len(cmd.Data))) + } + payload = append(payload, cmd.Data...) + frame = append(frame, payload...) + frame = append(frame, calCheckSum(payload)) + frame = append(frame, FrameFooter) + return frame +} + +// 解析接收帧 +func ParseFrame(data []byte) (interface{}, error) { + g.Log().Debugf("ParseFrame:[% X]", data) + s := "" + for i, d := range data { + if i == 0 { + + } + s += fmt.Sprintf("0x%X,", d) + } + //g.Log().Debugf("\n ParseFrame:{%s} ", s) + // 基本长度检查 + if len(data) < 5 { + return nil, + NewReadError("frame too short") + } + + // 验证帧头帧尾 + if data[0] != FrameHeader || data[len(data)-1] != FrameFooter { + return nil, NewReadError("invalid frame header or footer") + } + + // 提取有效载荷和校验和 + payload := data[1 : len(data)-2] + receivedCS := data[len(data)-2] + + // 计算校验和 + if calCheckSum(payload) != receivedCS { + return nil, NewReadError("checksum mismatch") + } + + // 解析指令类型 + if len(payload) < 2 { + return nil, NewReadError("invalid payload length") + } + + instruction := payload[0] + cmdType := payload[1] + + // 判断是否是响应帧(D7位是否置1) + isResponse := (instruction & ResponseFlag) != 0 + + switch { + case isResponse && cmdType == CmdRead: + // 特殊处理实际通断情况查询指令 (0x19) 的响应 + // 该指令返回的数据结构较为特殊,可能包含多个字段,需要专门的解析逻辑 + // 或者复用 parseReadResponse,但在解析 Data 时进行特定处理 + return parseReadResponse(payload) + case isResponse && cmdType == CmdWrite: + return parseWriteResponse(payload) + default: + return nil, terror.New("unsupported command type") + } +} + +// 解析读取响应 +func parseReadResponse(payload []byte) (*ReadResponse, error) { + if len(payload) < 4 { + return nil, terror.New("invalid read response format") + } + + index := payload[2] + dataLength := payload[3] + + if len(payload) < 4+int(dataLength) { + return nil, terror.New("data length mismatch") + } + + return &ReadResponse{ + Instruction: payload[0], + Index: index, + Data: payload[4 : 4+dataLength], + }, nil +} +func parseReadBlockResponse(payload []byte) (*ReadResponse, error) { + if len(payload) < 4 { + return nil, terror.New("invalid read response format") + } + + index := payload[2] + dataLength := payload[3:5] + le := BytesToUint16LE(dataLength) + if len(payload) < 4+int(le) { + return nil, terror.New("data length mismatch") + } + + return &ReadResponse{ + Instruction: payload[0], + Index: index, + Data: payload[5 : 5+le], + }, nil +} + +// 解析写入响应 +func parseWriteResponse(payload []byte) (*WriteResponse, error) { + if len(payload) < 5 { + return nil, terror.New("invalid write response format") + } + + index := payload[2] + dataLength := payload[3] + + if dataLength != 1 { + return nil, terror.New("invalid data length in write response") + } + + success := payload[4] == 0x01 + return &WriteResponse{ + Instruction: payload[0], + Index: index, + Success: success, + }, nil +} + +// 辅助函数:将数据转换为小端格式的uint16 +func BytesToUint16LE(b []byte) uint16 { + if len(b) < 2 { + return 0 + } + return uint16(b[0]) | uint16(b[1])<<8 +} + +// 辅助函数:将uint16转换为小端格式的字节切片 +func Uint16ToBytesLE(n uint16) []byte { + return []byte{byte(n), byte(n >> 8)} +} + +/* + func pushCommand(cmd []byte) ([]byte, error) { + push.Lock() + defer push.Unlock() + fmt.Printf("Push Command :% X\n", cmd) + conn, err := net.Dial("tcp", "10.10.100.254:8899") + if err != nil { + g.Log().Infof("%+v\n", err) + return nil, err + } + defer conn.Close() + _, err = conn.Write(cmd) + if err != nil { + g.Log().Infof("%+v\n", err) + return nil, err + } + buf := make([]byte, 128) + n, err := conn.Read(buf) + if err != nil { + g.Log().Infof("%+v\n", err) + return nil, err + } + return buf[:n], nil + } +*/ +func pushCommand(deviceId string, cmd []byte, detail string) ([]byte, error) { + + var result []byte + g.Log().Debugf("push cmd:[% X] ", cmd) + + err := maincontrollerClient.DeviceCallByDeviceID( + deviceId, + "/lampControl", + "", cmd, &result, func(deviceID, method string, params, result interface{}) (deviceType, cmdDetail, cmdDisplay, resultDisplay string) { + deviceType = "tgk" + cmdDetail = detail + return + }) + if err != nil { + return nil, terror.Wrapf(err, "单灯控制命令 deviceID={%v} cmd={% X} 下发失败", deviceId, cmd) + } + return result, nil + +} + +func ReadCmd(deviceId string, cmd M9zCtrl, detail string, idx ...uint) (*ReadResponse, error) { + var idxEnd byte + idxEnd = 0x00 + if len(idx) > 0 { + idxEnd = byte(idx[0]) + } + readCmd := ReadCommand{Instruction: byte(cmd), Index: idxEnd} + readFrame := BuildReadCommand(readCmd) + cmdRespData, err := pushCommand(deviceId, readFrame, detail) + if err != nil { + return nil, err + } + frame, err := ParseFrame(cmdRespData) + if err != nil { + return nil, err + } + return frame.(*ReadResponse), err +} + +// 定义专属的标记常量(根据业务场景选择合适的值,这里用0xff对应原逻辑的-1) 标记是升级 +const UpdateMarker byte = 0xff // 特殊标记值,替代原逻辑中的-1 + +func WriteCmd(deviceId string, cmd M9zCtrl, data []byte, detail string, idx ...uint8) (*WriteResponse, error) { + isUpdate := false + // 4. 构建写入命令 + wIdx := byte(0x00) + if len(idx) > 0 { + wIdx = idx[0] + // 准确判断是否是特殊标记值(类型完全匹配,无隐式转换) + if wIdx == UpdateMarker { + isUpdate = true + // 这里添加标记值的处理逻辑(根据你的业务需求定制) + // 示例:重置为默认值、执行特定业务逻辑等 + wIdx = 0x00 // 示例操作:将标记值重置为默认值 + } + } + writeCmd := WriteCommand{ + Instruction: byte(cmd), + Index: wIdx, + Data: data, + IsUpdate: isUpdate, + } + writeFrame := BuildWriteCommand(writeCmd) + + cmdRespData, err := pushCommand(deviceId, writeFrame, detail) + if err != nil { + return nil, err + } + resp, err := ParseFrame(cmdRespData) + if err != nil { + return nil, err + } + return resp.(*WriteResponse), nil +} +func percentToHex(percent int) byte { + if percent < 0 { + return 0x00 // 低于0%视为0% + } + if percent > 100 { + return 0x64 // 超过100%视为100% + } + return byte(percent) // 直接转换 +} +func hexToPercent(hexValue byte) int { + if hexValue > 0x64 { + err := fmt.Errorf("非法十六进制值: 0x%02X(超过0x64)", hexValue) + g.Log().Infof("%+v", err) + return 0 + } + return int(hexValue) +} diff --git a/internal/library/meterControl/Adl200Impl/init.go b/internal/library/meterControl/Adl200Impl/init.go new file mode 100644 index 0000000..5fb441a --- /dev/null +++ b/internal/library/meterControl/Adl200Impl/init.go @@ -0,0 +1,21 @@ +package Adl200Impl + +// Adl200Impl ADL200 单相电表控制器实现 +type Adl200Impl struct { +} + +// NewAdl200Impl 创建新的 ADL200 控制器实例 +func NewAdl200Impl() *Adl200Impl { + return &Adl200Impl{} +} + +// adl200 单例实例 +var adl200 *Adl200Impl + +// GetAdl200Impl 获取 ADL200 单例实例 +func GetAdl200Impl() *Adl200Impl { + if adl200 == nil { + adl200 = NewAdl200Impl() + } + return adl200 +} diff --git a/internal/library/meterControl/Adl200Impl/read.go b/internal/library/meterControl/Adl200Impl/read.go new file mode 100644 index 0000000..89eb0b2 --- /dev/null +++ b/internal/library/meterControl/Adl200Impl/read.go @@ -0,0 +1,304 @@ +package Adl200Impl + +import ( + "fmt" + "tgk-touch/internal/global" + "tgk-touch/internal/library/ADL200" + "time" +) + +// ===== 时间读取 ===== + +// ReadYear 读取电表年份 +func (a *Adl200Impl) ReadYear(commUid string, addr byte) (int, error) { + read, err := ADL200.H0012.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + // 高字节=年 (如 0x07 表示 2007) + year := int(byte(value >> 8)) + return year, nil +} + +// ReadMonth 读取电表月份 +func (a *Adl200Impl) ReadMonth(commUid string, addr byte) (int, error) { + read, err := ADL200.H0012.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + // 低字节=月 + month := int(byte(value)) + return month, nil +} + +// ReadDay 读取电表日期 +func (a *Adl200Impl) ReadDay(commUid string, addr byte) (int, error) { + read, err := ADL200.H0013.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + // 低字节=日 + day := int(byte(value)) + return day, nil +} + +// ReadHour 读取电表小时 +func (a *Adl200Impl) ReadHour(commUid string, addr byte) (int, error) { + read, err := ADL200.H0013.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + // 高字节=时 + hour := int(byte(value >> 8)) + return hour, nil +} + +// ReadMinute 读取电表分钟 +func (a *Adl200Impl) ReadMinute(commUid string, addr byte) (int, error) { + read, err := ADL200.H0014.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + // 低字节=分 + minute := int(byte(value)) + return minute, nil +} + +// ReadSecond 读取电表秒数 +func (a *Adl200Impl) ReadSecond(commUid string, addr byte) (int, error) { + read, err := ADL200.H0014.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + // 高字节=秒 + second := int(byte(value >> 8)) + return second, nil +} + +// ReadTime 读取电表完整时间 +func (a *Adl200Impl) ReadTime(commUid string, addr byte) (time.Time, error) { + // 读取各个时间组件 + year, err := a.ReadYear(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("读取年份失败: %v", err) + } + + month, err := a.ReadMonth(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("读取月份失败: %v", err) + } + + day, err := a.ReadDay(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("读取日期失败: %v", err) + } + + hour, err := a.ReadHour(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("读取小时失败: %v", err) + } + + minute, err := a.ReadMinute(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("读取分钟失败: %v", err) + } + + second, err := a.ReadSecond(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("读取秒数失败: %v", err) + } + + // 构建时间对象 (年份为 2 位数字,加 2000) + loc, _ := time.LoadLocation("Local") + t := time.Date( + year+2000, + time.Month(month), + day, + hour, + minute, + second, + 0, + loc, + ) + return t, nil +} + +// ===== 电气参数读取 ===== + +// ReadVoltage 读取电压 (单相电表只有单相电压) +func (a *Adl200Impl) ReadVoltage(commUid string, addr byte) (totalVoltage, aVoltage, bVoltage, cVoltage float64, err error) { + read, err := ADL200.H000B.Read(commUid, addr) + if err != nil { + return + } + // 单位: 0.1V + aVoltage = float64(read[0]) * 0.1 + // ADL200 是单相电表,总电压即为 A 相电压 + totalVoltage = aVoltage + return totalVoltage, aVoltage, bVoltage, cVoltage, err +} + +// ReadElectricCurrent 读取电流 (单相电表只有单相电流) +func (a *Adl200Impl) ReadElectricCurrent(commUid string, addr byte) (totalElectricCurrent, aElectricCurrent, bElectricCurrent, cElectricCurrent float64, err error) { + read, err := ADL200.H000C.Read(commUid, addr) + if err != nil { + return + } + // 单位: 0.01A + aElectricCurrent = float64(read[0]) * 0.01 + totalElectricCurrent = aElectricCurrent + return totalElectricCurrent, aElectricCurrent, bElectricCurrent, cElectricCurrent, err +} + +// ReadPower 读取功率 (单相电表只有总有功功率) +func (a *Adl200Impl) ReadPower(commUid string, addr byte) (totalPower, aPower, bPower, cPower float64, err error) { + read, err := ADL200.H000D.Read(commUid, addr) + if err != nil { + return + } + // 单位: 0.001kW + totalPower = float64(read[0]) * 0.001 + aPower = totalPower + return totalPower, aPower, bPower, cPower, err +} + +// ReadTotalPower 读取总有功功率 +func (a *Adl200Impl) ReadTotalPower(commUid string, addr byte) (totalPower float64, err error) { + read, err := ADL200.H000D.Read(commUid, addr) + if err != nil { + return 0, err + } + // 单位: 0.001kW + totalPower = float64(read[0]) * 0.001 + return totalPower, err +} + +// ReadEnergy 读取电能 (4 字节数据,需要两个寄存器) +func (a *Adl200Impl) ReadEnergy(commUid string, addr byte) (totalEnergy, aEnergy, bEnergy, cEnergy float64, err error) { + read, err := ADL200.H0068.Read(commUid, addr) + if err != nil { + return + } + // 高16位在前,低16位在后 (大端序) + high := uint32(read[0]) // 高16位 + low := uint32(read[1]) // 低16位 + value := (high << 16) | low // 组合为32位整数 + // 单位: 0.01kWh + totalEnergy = float64(value) * 0.01 + aEnergy = totalEnergy // 单相电表,A相电能即为总电能 + return totalEnergy, aEnergy, bEnergy, cEnergy, err +} + +// ReadPowerFactor 读取功率因数 (单相电表) +func (a *Adl200Impl) ReadPowerFactor(commUid string, addr byte) (totalPf, aPf, bPf, cPf float64, err error) { + read, err := ADL200.H0010.Read(commUid, addr) + if err != nil { + return + } + // 单位: 0.001 + aPf = float64(read[0]) * 0.001 + totalPf = aPf + return totalPf, aPf, bPf, cPf, err +} + +// ===== 其他读取 ===== + +// ReadMsgAddr 读取通信地址 +func (a *Adl200Impl) ReadMsgAddr(commUid string, addr byte) (msgAddr byte, err error) { + read, err := ADL200.H0015.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + // 高8位=通信地址 (0~254) + msgAddr = byte(value >> 8) + g.Log().Infof("ADL200 ReadMsgAddr raw: 0x%X, msgAddr: 0x%X (%d)", value, msgAddr, msgAddr) + return msgAddr, nil +} + +// ReadMainAddr 读取主控地址 (DL/T645 表号,暂不支持) +func (a *Adl200Impl) ReadMainAddr(commUid string, addr byte) (byte, error) { + // ADL200 使用表号代替主控地址,此处返回通信地址作为占位 + return a.ReadMsgAddr(commUid, addr) +} + +// ReadDailyFreezingTime 读取日冻结时间 (ADL200 不支持日冻结,返回默认) +func (a *Adl200Impl) ReadDailyFreezingTime(commUid string, addr byte) (dailyFreezingTime string, err error) { + // ADL200 单相电表不支持日冻结时间设置 + return "00:00", nil +} + +// ReadMonthlyFreezingTime 读取月冻结时间 (ADL200 不支持月冻结,返回默认) +func (a *Adl200Impl) ReadMonthlyFreezingTime(commUid string, addr byte) (day, hour string, err error) { + // ADL200 单相电表不支持月冻结时间设置 + return "01", "00", nil +} + +// ===== 额外的数据读取方法 ===== + +// ReadFrequency 读取电网频率 +func (a *Adl200Impl) ReadFrequency(commUid string, addr byte) (frequency float64, err error) { + read, err := ADL200.H0011.Read(commUid, addr) + if err != nil { + return 0, err + } + // 单位: 0.01Hz + frequency = float64(read[0]) * 0.01 + return frequency, nil +} + +// ReadReactivePower 读取无功功率 +func (a *Adl200Impl) ReadReactivePower(commUid string, addr byte) (reactivePower float64, err error) { + read, err := ADL200.H000E.Read(commUid, addr) + if err != nil { + return 0, err + } + // 单位: 0.001kvar + reactivePower = float64(read[0]) * 0.001 + return reactivePower, nil +} + +// ReadApparentPower 读取视在功率 +func (a *Adl200Impl) ReadApparentPower(commUid string, addr byte) (apparentPower float64, err error) { + read, err := ADL200.H000F.Read(commUid, addr) + if err != nil { + return 0, err + } + // 单位: 0.001kVA + apparentPower = float64(read[0]) * 0.001 + return apparentPower, nil +} + +// ReadCombinedEnergy 读取组合有功总电能 +func (a *Adl200Impl) ReadCombinedEnergy(commUid string, addr byte) (energy float64, err error) { + read, err := ADL200.H0000.Read(commUid, addr) + if err != nil { + return 0, err + } + high := uint32(read[0]) + low := uint32(read[1]) + value := (high << 16) | low + // 单位: 0.01kWh + energy = float64(value) * 0.01 + return energy, nil +} + +// ReadReactiveEnergy 读取总无功电能 +func (a *Adl200Impl) ReadReactiveEnergy(commUid string, addr byte) (energy float64, err error) { + read, err := ADL200.H00B0.Read(commUid, addr) + if err != nil { + return 0, err + } + high := uint32(read[0]) + low := uint32(read[1]) + value := (high << 16) | low + // 单位: 0.01kVarh + energy = float64(value) * 0.01 + return energy, nil +} diff --git a/internal/library/meterControl/Adl200Impl/write.go b/internal/library/meterControl/Adl200Impl/write.go new file mode 100644 index 0000000..e6196a1 --- /dev/null +++ b/internal/library/meterControl/Adl200Impl/write.go @@ -0,0 +1,168 @@ +package Adl200Impl + +import ( + "encoding/binary" + "tgk-touch/internal/library/ADL200" + "time" +) + +// ===== 时间写入 ===== + +// WriteYear 写入电表年份 +func (a *Adl200Impl) WriteYear(commUid string, addr byte, year int) error { + // 读取当前年月 + result, err := ADL200.H0012.Read(commUid, addr) + if err != nil { + return err + } + value := result[0] + // 高字节=年,低字节=月 + month := byte(value) + data := uint16(year)<<8 | uint16(month) + return ADL200.H0012.Write(commUid, addr, []uint16{data}) +} + +// WriteMonth 写入电表月份 +func (a *Adl200Impl) WriteMonth(commUid string, addr byte, month int) error { + // 读取当前年月 + result, err := ADL200.H0012.Read(commUid, addr) + if err != nil { + return err + } + value := result[0] + // 高字节=年,低字节=月 + year := byte(value >> 8) + data := uint16(year)<<8 | uint16(month) + return ADL200.H0012.Write(commUid, addr, []uint16{data}) +} + +// WriteDay 写入电表日期 +func (a *Adl200Impl) WriteDay(commUid string, addr byte, day int) error { + // 读取当前日时 + read, err := ADL200.H0013.Read(commUid, addr) + if err != nil { + return err + } + value := read[0] + // 高字节=时,低字节=日 + hour := byte(value >> 8) + data := make([]byte, 2) + data[0] = hour + data[1] = byte(day) + writeData := binary.BigEndian.Uint16(data) + return ADL200.H0013.Write(commUid, addr, []uint16{writeData}) +} + +// WriteHour 写入电表小时 +func (a *Adl200Impl) WriteHour(commUid string, addr byte, hour int) error { + // 读取当前日时 + read, err := ADL200.H0013.Read(commUid, addr) + if err != nil { + return err + } + value := read[0] + // 高字节=时,低字节=日 + day := byte(value) + data := make([]byte, 2) + data[0] = byte(hour) + data[1] = day + writeData := binary.BigEndian.Uint16(data) + return ADL200.H0013.Write(commUid, addr, []uint16{writeData}) +} + +// WriteMinute 写入电表分钟 +func (a *Adl200Impl) WriteMinute(commUid string, addr byte, minute int) error { + // 读取当前分秒 + read, err := ADL200.H0014.Read(commUid, addr) + if err != nil { + return err + } + value := read[0] + // 高字节=秒,低字节=分 + second := byte(value >> 8) + data := make([]byte, 2) + data[0] = second + data[1] = byte(minute) + writeData := binary.BigEndian.Uint16(data) + return ADL200.H0014.Write(commUid, addr, []uint16{writeData}) +} + +// WriteSecond 写入电表秒数 +func (a *Adl200Impl) WriteSecond(commUid string, addr byte, second int) error { + // 读取当前分秒 + read, err := ADL200.H0014.Read(commUid, addr) + if err != nil { + return err + } + value := read[0] + // 高字节=秒,低字节=分 + minute := byte(value) + data := make([]byte, 2) + data[0] = byte(second) + data[1] = minute + writeData := binary.BigEndian.Uint16(data) + return ADL200.H0014.Write(commUid, addr, []uint16{writeData}) +} + +// WriteTime 写入电表完整时间 +func (a *Adl200Impl) WriteTime(commUid string, addr byte, t time.Time) error { + err := a.WriteYear(commUid, addr, t.Year()-2000) + if err != nil { + return err + } + err = a.WriteMonth(commUid, addr, int(t.Month())) + if err != nil { + return err + } + err = a.WriteDay(commUid, addr, t.Day()) + if err != nil { + return err + } + err = a.WriteHour(commUid, addr, t.Hour()) + if err != nil { + return err + } + err = a.WriteMinute(commUid, addr, t.Minute()) + if err != nil { + return err + } + err = a.WriteSecond(commUid, addr, t.Second()) + if err != nil { + return err + } + return nil +} + +// ===== 通信地址写入 ===== + +// WriteMsgAddr 写入通信地址 +func (a *Adl200Impl) WriteMsgAddr(commUid string, addr byte, newAddr byte) error { + // 读取当前值 (高8位=地址,低8位=波特率) + read, err := ADL200.H0015.Read(commUid, addr) + if err != nil { + return err + } + currentValue := read[0] + // 低8位保留波特率设置 + currentBaud := byte(currentValue) + // 高8位=新地址,低8位=原波特率 + newValue := uint16(newAddr)<<8 | uint16(currentBaud) + return ADL200.H0015.Write(commUid, addr, []uint16{newValue}) +} + +// ===== 额外写入方法 ===== + +// WriteBacklightTime 写入背光时间 +// minute: 0-255分钟,0为常亮 +func (a *Adl200Impl) WriteBacklightTime(commUid string, addr byte, minute byte) error { + data := []uint16{uint16(minute)} + return ADL200.H0016.Write(commUid, addr, data) +} + +// WriteStatus 写入状态配置 +// bit0: 费率类型 0-非复费率, 1-复费率 +// bit1: 接线方向 0-上进下出, 1-下进上出 +// bit3: 视在功率 0-PQS, 1-RMS +func (a *Adl200Impl) WriteStatus(commUid string, addr byte, status byte) error { + return ADL200.H0045.Write(commUid, addr, []uint16{uint16(status)}) +} diff --git a/internal/library/meterControl/Adl400Impl/adl400Impl.go b/internal/library/meterControl/Adl400Impl/adl400Impl.go new file mode 100644 index 0000000..72328cc --- /dev/null +++ b/internal/library/meterControl/Adl400Impl/adl400Impl.go @@ -0,0 +1,4 @@ +package Adl400Impl + +type Adl400Impl struct { +} diff --git a/internal/library/meterControl/Adl400Impl/init.go b/internal/library/meterControl/Adl400Impl/init.go new file mode 100644 index 0000000..0074757 --- /dev/null +++ b/internal/library/meterControl/Adl400Impl/init.go @@ -0,0 +1,15 @@ +package Adl400Impl + +var MeterAdl400 string = "AEM-ADL400" +var adl *Adl400Impl + +func NewAdl400Impl() *Adl400Impl { + return &Adl400Impl{} +} + +func GetAdl400Impl() *Adl400Impl { + if adl == nil { + adl = NewAdl400Impl() + } + return adl +} diff --git a/internal/library/meterControl/Adl400Impl/read.go b/internal/library/meterControl/Adl400Impl/read.go new file mode 100644 index 0000000..8b655e6 --- /dev/null +++ b/internal/library/meterControl/Adl400Impl/read.go @@ -0,0 +1,355 @@ +package Adl400Impl + +import ( + "fmt" + "tgk-touch/internal/global" + "tgk-touch/internal/library/ADL400" + "time" +) + +func (a *Adl400Impl) ReadYear(commUid string, addr byte) (int, error) { + read, err := ADL400.H003E.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + + bytes := []byte{byte(value >> 8), byte(value)} + _, year := int(bytes[0]), int(bytes[1]) + return year, nil +} +func (a *Adl400Impl) ReadMonth(commUid string, addr byte) (int, error) { + read, err := ADL400.H003E.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] // 低字节为月份 + bytes := []byte{byte(value >> 8), byte(value)} + month, _ := int(bytes[0]), int(bytes[1]) + return month, nil +} +func (a *Adl400Impl) ReadDay(commUid string, addr byte) (int, error) { + read, err := ADL400.H003D.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + //hour := value >> 8 // 高字节=小时(9) + day := value & 0xFF // 低字节=日(27) + return int(day), nil +} +func (a *Adl400Impl) ReadHour(commUid string, addr byte) (int, error) { + read, err := ADL400.H003D.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + hour := value >> 8 // 高字节=小时(9) + //day := value & 0xFF // 低字节=日(27) + return int(hour), nil +} +func (a *Adl400Impl) ReadMinute(commUid string, addr byte) (int, error) { + read, err := ADL400.H003C.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + //second := value >> 8 // 高字节=小时(9) + minute := value & 0xFF // 低字节=日(27) + return int(minute), nil +} +func (a *Adl400Impl) ReadSecond(commUid string, addr byte) (int, error) { + read, err := ADL400.H003C.Read(commUid, addr) + if err != nil { + return 0, err + } + value := read[0] + second := value >> 8 // 高字节=小时(9) + //minute := value & 0xFF // 低字节=日(27) + return int(second), nil +} +func (a *Adl400Impl) ReadTime(commUid string, addr byte) (time.Time, error) { + // 读取各个时间组件 + year, err := a.ReadYear(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("failed to read year: %v", err) + } + + month, err := a.ReadMonth(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("failed to read month: %v", err) + } + + day, err := a.ReadDay(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("failed to read day: %v", err) + } + + hour, err := a.ReadHour(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("failed to read hour: %v", err) + } + + minute, err := a.ReadMinute(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("failed to read minute: %v", err) + } + + second, err := a.ReadSecond(commUid, addr) + if err != nil { + return time.Time{}, fmt.Errorf("failed to read second: %v", err) + } + // 构建时间对象 + loc, _ := time.LoadLocation("Local") // 使用本地时区 + t := time.Date( + year+2000, // 假设年份是2位数,加上2000 + time.Month(month), + day, + hour, + minute, + second, + 0, // 纳秒 + loc, + ) + return t, nil +} + +func (a *Adl400Impl) ReadVoltage(commUid string, addr byte) (totalVoltage, aVoltage, bVoltage, cVoltage float64, err error) { + aRead, err := ADL400.H0061.Read(commUid, addr) + if err != nil { + return + } + aVoltage = float64(aRead[0]) * 0.1 + bRead, err := ADL400.H0062.Read(commUid, addr) + if err != nil { + return + } + bVoltage = float64(bRead[0]) * 0.1 + cRead, err := ADL400.H0063.Read(commUid, addr) + if err != nil { + return + } + cVoltage = float64(cRead[0]) * 0.1 + return totalVoltage, aVoltage, bVoltage, cVoltage, err +} +func (a *Adl400Impl) ReadElectricCurrent(commUid string, addr byte) (totalElectricCurrent, aElectricCurrent, bElectricCurrent, cElectricCurrent float64, err error) { + aRead, err := ADL400.H0064.Read(commUid, addr) + if err != nil { + return + } + aElectricCurrent = float64(aRead[0]) * 0.01 + bRead, err := ADL400.H0065.Read(commUid, addr) + if err != nil { + return + } + bElectricCurrent = float64(bRead[0]) * 0.01 + + cRead, err := ADL400.H0066.Read(commUid, addr) + if err != nil { + return + } + cElectricCurrent = float64(cRead[0]) * 0.01 + + return totalElectricCurrent, aElectricCurrent, bElectricCurrent, cElectricCurrent, err +} +func (a *Adl400Impl) ReadPower(commUid string, addr byte) (totalPower, aPower, bPower, cPower float64, err error) { + aRead, err := ADL400.H0067.Read(commUid, addr) + if err != nil { + return + } + aPower = float64(aRead[0]) * 0.001 + bRead, err := ADL400.H0068.Read(commUid, addr) + if err != nil { + return + } + bPower = float64(bRead[0]) * 0.001 + + cRead, err := ADL400.H0069.Read(commUid, addr) + if err != nil { + return + } + cPower = float64(cRead[0]) * 0.001 + tRead, err := ADL400.H006A.Read(commUid, addr) + if err != nil { + return 0, 0, 0, 0, err + } + totalPower = float64(tRead[0]) * 0.001 + return totalPower, aPower, bPower, cPower, err +} +func (a *Adl400Impl) ReadTotalPower(commUid string, addr byte) (totalPower float64, err error) { + tRead, err := ADL400.H006A.Read(commUid, addr) + if err != nil { + return 0, err + } + totalPower = float64(tRead[0]) * 0.001 + return totalPower, err +} +func (a *Adl400Impl) ReadEnergy(commUid string, addr byte) (totalEnergy, aEnergy, bEnergy, cEnergy float64, err error) { + read, err := ADL400.H000A.Read(commUid, addr) + if err != nil { + return + } + high := uint32(read[0]) // 高16位 + low := uint32(read[1]) // 低16位 + value := (high << 16) | low // 组合为32位整数 + totalEnergy = float64(value) * 0.01 + + aRead, err := ADL400.H0087.Read(commUid, addr) + if err != nil { + return + } + ahigh := uint32(aRead[0]) // 高16位 + alow := uint32(aRead[1]) // 低16位 + avalue := (ahigh << 16) | alow // 组合为32位整数 + aEnergy = float64(avalue) * 0.01 + + bRead, err := ADL400.H0089.Read(commUid, addr) + if err != nil { + return + } + bhigh := uint32(bRead[0]) // 高16位 + blow := uint32(bRead[1]) // 低16位 + bvalue := (bhigh << 16) | blow // 组合为32位整数 + bEnergy = float64(bvalue) * 0.01 + + cRead, err := ADL400.H008B.Read(commUid, addr) + if err != nil { + return + } + chigh := uint32(cRead[0]) // 高16位 + clow := uint32(cRead[1]) // 低16位 + cvalue := (chigh << 16) | clow // 组合为32位整数 + cEnergy = float64(cvalue) * 0.01 + + return totalEnergy, aEnergy, bEnergy, cEnergy, err +} + +func (a *Adl400Impl) ReadPowerFactor(commUid string, addr byte) (totalPf, aPf, bPf, cPf float64, err error) { + aRead, err := ADL400.H0073.Read(commUid, addr) + if err != nil { + return + } + aPf = float64(aRead[0]) * 0.001 + bRead, err := ADL400.H0074.Read(commUid, addr) + if err != nil { + return + } + bPf = float64(bRead[0]) * 0.001 + + cRead, err := ADL400.H0075.Read(commUid, addr) + if err != nil { + return + } + cPf = float64(cRead[0]) * 0.001 + tRead, err := ADL400.H0076.Read(commUid, addr) + if err != nil { + return 0, 0, 0, 0, err + } + totalPf = float64(tRead[0]) * 0.001 + return totalPf, aPf, bPf, cPf, err +} +func (a *Adl400Impl) ReadMsgAddr(commUid string, addr byte) (msgAddr byte, err error) { + read, err := ADL400.H003F.Read(commUid, addr) + if err != nil { + return 0, err + } + + // 调试日志:打印原始数据 + g.Log().Infof("ReadMsgAddr raw data: len=%d, data=% X", len(read), read) + + // 根据实际数据长度处理不同情况 + switch len(read) { + case 1: // 单个 uint16 值 (2字节) + // 示例日志: [5C03] -> 0x5C03 + value := read[0] // 假设返回的是 uint16 类型 + msgAddr = byte(value >> 8) // 取高字节 (0x5C) + g.Log().Infof("Extracted from uint16: 0x%X -> msgAddr=0x%X (%d)", value, msgAddr, msgAddr) + /* + case 2: // 两个字节 [high, low] + // 示例: [5C 03] -> msgAddr=0x5C + msgAddr = read[0] + g.Log().Infof("Extracted from bytes: [% X] -> msgAddr=0x%X (%d)", read, msgAddr, msgAddr) + + default: + return 0, fmt.Errorf("unexpected data length: %d bytes", len(read))*/ + } + + return msgAddr, nil +} + +func (a *Adl400Impl) ReadMainAddr(commUid string, addr byte) (mainAddr byte, err error) { + /* read, err := ADL400.H0099_009E.Read(commUid, addr) + if err != nil { + return "", err + } + + // 调试日志 + g.Log().Infof("ReadMainAddr raw data: len=%d, data=% X", len(read), read) + + // 根据实际数据长度处理 + var bcdData []byte + switch v := read.(type) { + case []uint16: // 返回的是 uint16 数组 + // 示例: [0000 0000 0050] -> BCD "000000000050" + for _, reg := range v { + bcdData = append(bcdData, byte(reg>>8), byte(reg)) + } + + case []byte: // 直接返回字节数组 + bcdData = v + + default: + return "", fmt.Errorf("unexpected data type: %T", read) + } + + // 验证数据长度 + if len(bcdData) != 12 { + return "", fmt.Errorf("invalid BCD data length: expected 12 bytes, got %d", len(bcdData)) + } + + // 转换BCD码为字符串 + var builder strings.Builder + for _, b := range bcdData { + high := b >> 4 + low := b & 0x0F + + if high > 9 || low > 9 { + return "", fmt.Errorf("invalid BCD digit: %X", b) + } + + builder.WriteByte('0' + high) + builder.WriteByte('0' + low) + } + + mainAddr = builder.String() + g.Log().Infof("Converted mainAddr: %s", mainAddr)*/ + return +} + +func (a *Adl400Impl) ReadDailyFreezingTime(commUid string, addr byte) (dailyFreezingTime string, err error) { + read, err := ADL400.H0121.Read(commUid, addr) + if err != nil { + return "", err + } + value := read[0] + minute := value >> 8 // 高字节=小时(9) + hour := value & 0xFF // 低字节=日(27) + // 校验有效时间范围 + if hour < 0 || hour > 23 || minute < 0 || minute > 59 { + return fmt.Sprintf("%03d:%02d", hour, minute), nil + } + return fmt.Sprintf("%02d:%02d", hour, minute), nil + +} + +func (a *Adl400Impl) ReadMonthlyFreezingTime(commUid string, addr byte) (dayStr, hourStr string, err error) { + read, err := ADL400.H0122.Read(commUid, addr) + if err != nil { + return "", "", err + } + value := read[0] + day := value >> 8 // 高字节=小时(9) + hour := value & 0xFF // 低字节=日(27) + + return fmt.Sprintf("%02d", day), fmt.Sprintf("%02d", hour), err +} diff --git a/internal/library/meterControl/Adl400Impl/write.go b/internal/library/meterControl/Adl400Impl/write.go new file mode 100644 index 0000000..1911cae --- /dev/null +++ b/internal/library/meterControl/Adl400Impl/write.go @@ -0,0 +1,150 @@ +package Adl400Impl + +import ( + "encoding/binary" + "tgk-touch/internal/library/ADL400" + "time" +) + +func (a *Adl400Impl) WriteYear(commUid string, addr byte, year int) error { + // 1. 先读取当前月份(假设月份在低字节,年份在高字节) + result, err := ADL400.H003E.Read(commUid, addr) + if err != nil { + return err + } + value := result[0] // 低字节为月份 + bytes := []byte{byte(value >> 8), byte(value)} + month, yearRead := int(bytes[0]), int(bytes[1]) + yearRead = year + data := uint16(month)<<8 | uint16(yearRead) + // 3. 写入合并后的数据 + return ADL400.H003E.Write(commUid, addr, []uint16{data}) +} +func (a *Adl400Impl) WriteMonth(commUid string, addr byte, month int) error { + // 1. 先读取当前月份(假设月份在低字节,年份在高字节) + result, err := ADL400.H003E.Read(commUid, addr) + if err != nil { + return err + } + value := result[0] // 低字节为月份 + bytes := []byte{byte(value >> 8), byte(value)} + monthRead, year := int(bytes[0]), int(bytes[1]) + monthRead = month + data := uint16(monthRead)<<8 | uint16(year) + // 3. 写入合并后的数据 + return ADL400.H003E.Write(commUid, addr, []uint16{data}) +} +func (a *Adl400Impl) WriteDay(commUid string, addr byte, day int) error { + read, err := ADL400.H003D.Read(commUid, addr) + if err != nil { + return err + } + value := read[0] + hour := int(value >> 8) // 高字节=小时(9) + data := make([]byte, 2) + data[0] = byte(hour) // 高字节 = 时 + data[1] = byte(day) // 低字节 = 日 + writeData := binary.BigEndian.Uint16(data) + err = ADL400.H003D.Write(commUid, addr, []uint16{writeData}) + if err != nil { + return err + } + return nil +} +func (a *Adl400Impl) WriteHour(commUid string, addr byte, hour int) error { + read, err := ADL400.H003D.Read(commUid, addr) + if err != nil { + return err + } + value := read[0] + day := value & 0xFF // 高字节=小时(9) + data := make([]byte, 2) + data[0] = byte(hour) // 高字节 = 时 + data[1] = byte(day) // 低字节 = 日 + writeData := binary.BigEndian.Uint16(data) + err = ADL400.H003D.Write(commUid, addr, []uint16{writeData}) + if err != nil { + return err + } + return nil +} +func (a *Adl400Impl) WriteMinute(commUid string, addr byte, minute int) error { + read, err := ADL400.H003C.Read(commUid, addr) + if err != nil { + return err + } + value := read[0] + //log.Printf("value = %04X", value) + + second := value >> 8 // 高字节=小时(9) + //minuteRead := value & 0xFF // 低字节=日(27) + data := make([]byte, 2) + data[0] = byte(second) // 高字节 = 时 + data[1] = byte(minute) // 低字节 = 日 + writeData := binary.BigEndian.Uint16(data) + //log.Printf("writeData = %04X", writeData) + return ADL400.H003C.Write(commUid, addr, []uint16{writeData}) + +} +func (a *Adl400Impl) WriteSecond(commUid string, addr byte, second int) error { + read, err := ADL400.H003C.Read(commUid, addr) + if err != nil { + return err + } + value := read[0] + //second := value >> 8 // 高字节=小时(9) + minute := value & 0xFF // 低字节=日(27) + data := make([]byte, 2) + data[0] = byte(second) // 高字节 = 时 + data[1] = byte(minute) // 低字节 = 日 + writeData := binary.BigEndian.Uint16(data) + err = ADL400.H003C.Write(commUid, addr, []uint16{writeData}) + if err != nil { + return err + } + return nil +} +func (a *Adl400Impl) WriteTime(commUid string, addr byte, t time.Time) error { + err := a.WriteYear(commUid, addr, t.Year()-2000) + if err != nil { + return err + } + err = a.WriteMonth(commUid, addr, int(t.Month())) + if err != nil { + return err + } + err = a.WriteHour(commUid, addr, t.Hour()) + if err != nil { + return err + } + err = a.WriteMinute(commUid, addr, t.Minute()) + if err != nil { + return err + } + err = a.WriteSecond(commUid, addr, t.Second()) + if err != nil { + return err + } + return nil +} + +func (a *Adl400Impl) WriteMsgAddr(commUid string, addr byte, newAddr byte) error { + + // 1. 先读取当前003FH寄存器的完整值 + read, err := ADL400.H003F.Read(commUid, addr) + if err != nil { + return err + } + + // 2. 解析当前值(假设返回uint16) + currentValue := read[0] + currentBaud := byte(currentValue & 0xFF) // 低字节=波特率 + + // 3. 构建新值:新地址(高8位) + 原波特率(低8位) + newValue := uint16(newAddr)<<8 | uint16(currentBaud) + + // 4. 写入新值 + writeData := []uint16{newValue} + return ADL400.H003F.Write(commUid, addr, writeData) + +} diff --git a/internal/library/meterControl/init.go b/internal/library/meterControl/init.go new file mode 100644 index 0000000..76dd028 --- /dev/null +++ b/internal/library/meterControl/init.go @@ -0,0 +1,50 @@ +package meterControl + +import "tgk-touch/internal/library/meterControl/Adl200Impl" +import "tgk-touch/internal/library/meterControl/Adl400Impl" + +type MeterFirm string + +const ( + ADL200 MeterFirm = "AEM-ADL200" + ADL400 MeterFirm = "AEM-ADL400" +) + +var ( + meterControls = make(map[MeterFirm]IMeterControl) + readMeterControls = make(map[MeterFirm]IReadMeter) + writeMeterControls = make(map[MeterFirm]IWriteMeter) +) + +func init() { + // 注册 ADL200 单相电表控制器 + meterControls[ADL200] = Adl200Impl.GetAdl200Impl() + readMeterControls[ADL200] = Adl200Impl.GetAdl200Impl() + writeMeterControls[ADL200] = Adl200Impl.GetAdl200Impl() + + // 注册 ADL400 三相电表控制器 + meterControls[ADL400] = Adl400Impl.GetAdl400Impl() + readMeterControls[ADL400] = Adl400Impl.GetAdl400Impl() + writeMeterControls[ADL400] = Adl400Impl.GetAdl400Impl() +} + +func GetMeterControls(firm MeterFirm) IMeterControl { + if v, ok := meterControls[firm]; ok { + return v + } + return nil +} + +func GetReadMeterControls(firm MeterFirm) IReadMeter { + if v, ok := readMeterControls[firm]; ok { + return v + } + return nil +} + +func GetWriteMeterControls(firm MeterFirm) IWriteMeter { + if v, ok := writeMeterControls[firm]; ok { + return v + } + return nil +} diff --git a/internal/library/meterControl/meter_interface.go b/internal/library/meterControl/meter_interface.go new file mode 100644 index 0000000..b9fab35 --- /dev/null +++ b/internal/library/meterControl/meter_interface.go @@ -0,0 +1,49 @@ +package meterControl + +import "time" + +type IMeterControl interface { + IReadMeter + IWriteMeter +} +type IReadMeter interface { + // 获取电表上年份 + ReadYear(commUid string, addr byte) (int, error) + ReadMonth(commUid string, addr byte) (int, error) + ReadDay(commUid string, addr byte) (int, error) + ReadHour(commUid string, addr byte) (int, error) + ReadMinute(commUid string, addr byte) (int, error) + ReadSecond(commUid string, addr byte) (int, error) + ReadTime(commUid string, addr byte) (time.Time, error) + // ReadVoltage 读取电压 []float64 总电压,1相,2相,3相.... + ReadVoltage(commUid string, addr byte) (totalVoltage, aVoltage, bVoltage, cVoltage float64, err error) + // ReadElectricCurrent 读取电流 []float64 总电流,1相,2相,3相.... + ReadElectricCurrent(commUid string, addr byte) (totalElectricCurrent, aElectricCurrent, bElectricCurrent, cElectricCurrent float64, err error) + // ReadPower 读取功率 []float64 总功率,1相,2相,3相.... + ReadPower(commUid string, addr byte) (totalPower, aPower, bPower, cPower float64, err error) + // ReadTotalPower 读取当前总功率 + ReadTotalPower(commUid string, addr byte) (totalPower float64, err error) + // ReadEnergy 读取功率 []float64 总电能,1相,2相,3相.... + ReadEnergy(commUid string, addr byte) (totalEnergy, aEnergy, bEnergy, cEnergy float64, err error) + // ReadPowerFactor 读取功率因数 []float64 总功率因数,1相,2相,3相.... + ReadPowerFactor(commUid string, addr byte) (totalPf, aPf, bPf, cPf float64, err error) + // ReadMsgAddr 读取通信地址 + ReadMsgAddr(commUid string, addr byte) (byte, error) + // ReadMainAddr 读取主控地址 + ReadMainAddr(commUid string, addr byte) (byte, error) + // ReadDailyFreezingTime 读取日冻结时间 + ReadDailyFreezingTime(commUid string, addr byte) (string, error) + // ReadMonthlyFreezingTime 读取月冻结时间 + ReadMonthlyFreezingTime(commUid string, addr byte) (day, hour string, err error) +} +type IWriteMeter interface { + // 获取电表上年份 + WriteYear(commUid string, addr byte, year int) error + WriteMonth(commUid string, addr byte, month int) error + WriteDay(commUid string, addr byte, day int) error + WriteHour(commUid string, addr byte, hour int) error + WriteMinute(commUid string, addr byte, minute int) error + WriteSecond(commUid string, addr byte, second int) error + WriteTime(commUid string, addr byte, t time.Time) error + WriteMsgAddr(commUid string, addr byte, newAddr byte) error +} diff --git a/internal/model/config/config.go b/internal/model/config/config.go new file mode 100644 index 0000000..91f8b99 --- /dev/null +++ b/internal/model/config/config.go @@ -0,0 +1,150 @@ +package config + +import "github.com/gogf/gf/v2/database/gdb" + +// 使用 mapstructure 标签可以指定 confgi 的 key 名 + +type ( + AppConfig struct { + Server Server `json:"server" mapstructure:"server"` + Databases Databases `json:"databases" mapstructure:"databases"` + MainControllerClient MainControllerClient `json:"mainControllerClient" mapstructure:"mainControllerClient"` + Togocdn Togocdn `json:"togocdn" mapstructure:"togocdn"` + Logger Logger `json:"logger" mapstructure:"logger"` + LicenseConf LicenseConf `json:"licenseConf" mapstructure:"licenseConf"` + Zap Zap `json:"zap" mapstructure:"zap"` + Tty Tty `json:"tty" mapstructure:"tty"` + DeviceInfo DeviceInfo `json:"deviceInfo" mapstructure:"deviceInfo"` + Firefox Firefox `json:"firefox" mapstructure:"firefox"` + MessageInterval int `json:"MessageInterval" mapstructure:"MessageInterval"` + IotServer IotServer `json:"iotServer" mapstructure:"iotServer"` + RabbitMq RabbitMq `json:"rabbitMq" mapstructure:"rabbitMq"` + LiKong LiKong `json:"liKong" mapstructure:"li-kong"` + HuYi HuYi `json:"huYi" mapstructure:"hu-yi"` + AliSms AliSms `json:"aliSms" mapstructure:"ali-sms"` + Gdb gdb.Config `json:"gdb" mapstructure:"gdb"` + } +) + +type ( + Server struct { + Port int `json:"port" mapstructure:"port"` + Static string `json:"static" mapstructure:"static"` + IsLocal bool `json:"isLocal" mapstructure:"is-local"` + } + + Databases struct { + Mode string `json:"mode" mapstructure:"mode"` + Nodes []DatabasesNode `json:"nodes" mapstructure:"nodes"` + } + DatabasesNode struct { + DbType string `json:"DbType" mapstructure:"DbType"` + IsMaster bool `json:"IsMaster" mapstructure:"IsMaster"` + Dsn string `json:"Dsn" mapstructure:"Dsn"` + SqlMaxIdleConns int `json:"sqlMaxIdleConns" mapstructure:"sqlMaxIdleConns"` + SqlMaxOpenConns int `json:"sqlMaxOpenConns" mapstructure:"sqlMaxOpenConns"` + SqlLogLevel int `json:"sqlLogLevel" mapstructure:"sqlLogLevel"` + } + + MainControllerClient struct { + ServerHost string `json:"server_host" mapstructure:"server_host"` + SecretKey string `json:"secret_key" mapstructure:"secret_key"` + } + + Togocdn struct { + Client struct { + Priority int `json:"Priority" mapstructure:"Priority"` + ModuleName string `json:"ModuleName" mapstructure:"ModuleName"` + ServerUrls []string `json:"ServerUrls" mapstructure:"ServerUrls"` + } `json:"client" mapstructure:"client"` + } + + Logger struct { + Path string `json:"path" mapstructure:"path"` + File string `json:"file" mapstructure:"file"` + Prefix string `json:"prefix" mapstructure:"prefix"` + Level string `json:"level" mapstructure:"level"` + TimeFormat string `json:"timeFormat" mapstructure:"timeFormat"` + CtxKeys []interface{} `json:"ctxKeys" mapstructure:"ctxKeys"` + Header bool `json:"header" mapstructure:"header"` + Stdout bool `json:"stdout" mapstructure:"stdout"` + RotateSize int `json:"rotateSize" mapstructure:"rotateSize"` + RotateExpire string `json:"rotateExpire" mapstructure:"rotateExpire"` + RotateBackupLimit int `json:"rotateBackupLimit" mapstructure:"rotateBackupLimit"` + RotateBackupExpire string `json:"rotateBackupExpire" mapstructure:"rotateBackupExpire"` + RotateBackupCompress int `json:"rotateBackupCompress" mapstructure:"rotateBackupCompress"` + RotateCheckInterval string `json:"rotateCheckInterval" mapstructure:"rotateCheckInterval"` + StdoutColorDisabled bool `json:"stdoutColorDisabled" mapstructure:"stdoutColorDisabled"` + WriterColorEnable bool `json:"writerColorEnable" mapstructure:"writerColorEnable"` + } + + LicenseConf struct { + LicenseServerUrl string `json:"LicenseServerUrl" mapstructure:"LicenseServerUrl"` + LicenseKey string `json:"LicenseKey" mapstructure:"LicenseKey"` + } + + Tty struct { + SerialPortAddress string `json:"serialPortAddress" mapstructure:"serialPortAddress"` + BaudRate int `json:"baudRate" mapstructure:"baudRate"` + } + + DeviceInfo struct { + DeviceId string `json:"deviceId" mapstructure:"deviceId"` + Version string `json:"version" mapstructure:"version"` + MeterAddr string `json:"meterAddr" mapstructure:"meterAddr"` + } + + Firefox struct { + DebPkgPath string `json:"DebPkgPath" mapstructure:"DebPkgPath"` + DebPkgName string `json:"DebPkgName" mapstructure:"DebPkgName"` + InstallShellPath string `json:"InstallShellPath" mapstructure:"InstallShellPath"` + } + + IotServer struct { + ServerUrl string `json:"serverUrl" mapstructure:"serverUrl"` + Port string `json:"port" mapstructure:"port"` + } + + RabbitMq struct { + Server string `json:"server" mapstructure:"server"` + Port int `json:"port" mapstructure:"port"` + Username string `json:"username" mapstructure:"username"` + Password string `json:"password" mapstructure:"password"` + } + + // LiKong 接口客户端 + LiKong struct { + BaseURL string `json:"baseURL" mapstructure:"base-url"` + TenantID string `json:"tenantID" mapstructure:"tenant-id"` + ClientID string `json:"clientID" mapstructure:"client-id"` + ClientSecret string `json:"clientSecret" mapstructure:"client-secret"` + Username string `json:"username" mapstructure:"username"` + Password string `json:"password" mapstructure:"password"` + ApiPrefix string `json:"api-prefix" mapstructure:"api-prefix"` + } + HuYi struct { + BaseURL string `json:"baseURL" mapstructure:"base-url"` + AppId string `json:"appID" mapstructure:"app-id"` + AppKey string `json:"appKey" mapstructure:"app-key"` + TemplateId string `json:"templateId" mapstructure:"template-id"` + } + AliSms struct { + // AccessKeyID RAM账号AccessKey ID + AccessKeyID string `json:"accessKeyID" mapstructure:"access-key-id"` + + // AccessKeySecret RAM账号AccessKey Secret + AccessKeySecret string `json:"accessKeySecret" mapstructure:"access-key-secret"` + + // SignName 短信签名(审核通过的名称) + SignName string `json:"signName" mapstructure:"sign-name"` + + // WarnTemplateId 告警短信模板ID + WarnTemplateId string `json:"warnTemplateId" mapstructure:"warn-template-id"` + + // InfoTemplateId 信息短信模板ID + InfoTemplateId string `json:"infoTemplateId" mapstructure:"info-template-id"` + + // Endpoint 短信服务接入地址(默认:dysmsapi.aliyuncs.com) + Endpoint string `json:"endpoint" mapstructure:"endpoint"` + } +) diff --git a/internal/model/config/zap.go b/internal/model/config/zap.go new file mode 100644 index 0000000..ea4b0e4 --- /dev/null +++ b/internal/model/config/zap.go @@ -0,0 +1,99 @@ +package config + +import ( + "go.uber.org/zap/zapcore" + "time" +) + +type Zap struct { + // 级别 info debug warn error dpanic panic fatal + Level string `mapstructure:"level" json:"level" yaml:"level"` + Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 日志前缀 + // 输出 console: 控制台形式输出日志 json: json格式输出日志 + Format string `mapstructure:"format" json:"format" yaml:"format"` + // 日志文件夹 存放日志的文件夹,修改即可,不需要手动创建 + Director string `mapstructure:"director" json:"director" yaml:"director"` + // 编码级 LowercaseLevelEncoder:小写 LowercaseColorLevelEncoder:小写带颜色 CapitalColorLevelEncoder: 大写带颜色 CapitalLevelEncoder: 大写 + EncodeLevel string `mapstructure:"encode-level" json:"encode-level" yaml:"encode-level"` + StacktraceKey string `mapstructure:"stacktrace-key" json:"stacktrace-key" yaml:"stacktrace-key"` // 栈名 + ShowLine bool `mapstructure:"show-line" json:"show-line" yaml:"show-line"` // 显示行 显示行号, 默认为true,不建议修改 + LogInConsole bool `mapstructure:"log-in-console" json:"log-in-console" yaml:"log-in-console"` // 输出控制台 + ConsoleInLog bool `mapstructure:"console-in-log" json:"console-in-log" yaml:"console-in-log"` // 输出控制台 + RetentionDay int `mapstructure:"retention-day" json:"retention-day" yaml:"retention-day"` // 日志保留天数 + MaxSize string `mapstructure:"max-size" json:"maxSize"` // 单个日志文件最大大小 1kb, 1mb 1g + + Compress bool `mapstructure:"compress" json:"compress"` // 切割后是否压缩旧文件(gzip) + CompressLevel int `mapstructure:"compress-level" json:"compress-level"` // gzip 压缩等级(1-9) + CheckInterval string `mapstructure:"check-interval" json:"check-interval"` // 定时检查切割的时间间隔 定时检查间隔(如"30s"、"1m",默认30秒) +} + +// Levels 根据字符串转化为 zapcore.Levels +func (c *Zap) Levels() []zapcore.Level { + levels := make([]zapcore.Level, 0, 7) + level, err := zapcore.ParseLevel(c.Level) + if err != nil { + level = zapcore.DebugLevel + } + for ; level <= zapcore.FatalLevel; level++ { + levels = append(levels, level) + } + return levels +} + +func (c *Zap) Encoder() zapcore.Encoder { + config := zapcore.EncoderConfig{ + TimeKey: "time", + NameKey: "name", + LevelKey: "level", + CallerKey: "caller", + MessageKey: "message", + StacktraceKey: c.StacktraceKey, + LineEnding: zapcore.DefaultLineEnding, + EncodeTime: func(t time.Time, encoder zapcore.PrimitiveArrayEncoder) { + encoder.AppendString(c.Prefix + t.Format("2006-01-02 15:04:05.000")) + }, + EncodeLevel: c.LevelEncoder(), + EncodeCaller: zapcore.FullCallerEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + } + if c.Format == "json" { + return zapcore.NewJSONEncoder(config) + } + return zapcore.NewConsoleEncoder(config) + +} + +// LevelEncoder 根据 EncodeLevel 返回 zapcore.LevelEncoder +// Author [SliverHorn](https://github.com/SliverHorn) +func (c *Zap) LevelEncoder() zapcore.LevelEncoder { + switch { + case c.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认) + return zapcore.LowercaseLevelEncoder + case c.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色 + return zapcore.LowercaseColorLevelEncoder + case c.EncodeLevel == "CapitalLevelEncoder": // 大写编码器 + return zapcore.CapitalLevelEncoder + case c.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色 + return zapcore.CapitalColorLevelEncoder + default: + return zapcore.LowercaseLevelEncoder + } +} + +// 新增自定义调用者编码器方法,用于输出相对路径 +func (c *Zap) CallerEncoder() zapcore.CallerEncoder { + return func(caller zapcore.EntryCaller, encoder zapcore.PrimitiveArrayEncoder) { + // 获取项目根目录(根据实际项目结构调整,这里假设根目录是模块名,如 "your/project/root") + // 注意:需要替换为你的项目实际根目录(可以通过 go.mod 中的模块名确定) + projectRoot := "lampserver-10" + + // 截取相对路径(从项目根目录开始) + fullPath := caller.String() + relativePath := fullPath + if len(fullPath) >= len(projectRoot) { + relativePath = fullPath[len(projectRoot)+1:] // +1 是为了去掉根目录后的斜杠 + } + + encoder.AppendString(relativePath) + } +} diff --git a/internal/module/adl400TtyApi/init.go b/internal/module/adl400TtyApi/init.go new file mode 100644 index 0000000..1bcee7a --- /dev/null +++ b/internal/module/adl400TtyApi/init.go @@ -0,0 +1,48 @@ +// Package adl400Api 电表接口 +package adl400TtyApi + +import ( + "encoding/json" + "github.com/gogf/gf/v2/util/gconv" + "github.com/towgo/towgo/towgo" + "strings" + "tgk-touch/internal/global" + "tgk-touch/internal/library/meterControl" +) + +var mc meterControl.IMeterControl + +var deviceInfo map[string]interface{} + +func Init() { + marshal, _ := json.Marshal(g.Config().DeviceInfo) + json.Unmarshal(marshal, &deviceInfo) + + mc = meterControl.GetMeterControls(meterControl.ADL400) + initReadFunc() + initWriteFunc() + +} +func initReadFunc() { + towgo.SetFunc("/adl400/readPhaseData", readPhaseData) + // 读取通信地址 + towgo.SetFunc("/adl400/getMsgAddress", getMsgAddress) + // 读取日冻结时间 + towgo.SetFunc("/adl400/getDailyFreezingTime", getDailyFreezingTime) + // 读取月冻结时间 + towgo.SetFunc("/adl400/getMonthlyFreezingTime", getReadMonthlyFreezingTime) + // 读取冻结数据 + towgo.SetFunc("/adl400/getFreezeTheData", getFreezeTheData) + // 读取时间 + towgo.SetFunc("/adl400/geTime", geDeviceTime) +} +func initWriteFunc() { + // 写入通信地址 + towgo.SetFunc("/adl400/setMsgAddress", setMsgAddress) +} +func getMeterAddr(cuid string) byte { + + // 去掉 "0x" 前缀,按 16 进制解析 + return gconv.Byte(strings.TrimPrefix(deviceInfo["meterAddr"].(string), "0x")) + +} diff --git a/internal/module/adl400TtyApi/read.go b/internal/module/adl400TtyApi/read.go new file mode 100644 index 0000000..91dfd44 --- /dev/null +++ b/internal/module/adl400TtyApi/read.go @@ -0,0 +1,308 @@ +package adl400TtyApi + +import ( + "fmt" + "github.com/towgo/towgo/errors/terror" + "github.com/towgo/towgo/towgo" + "math" + "tgk-touch/internal/global" + "tgk-touch/internal/library/ADL400" + "tgk-touch/internal/module/basicInfo/deviceManagement" + "time" +) + +func readPhaseData(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + } + rpc.ReadParams(&p) + + addr := getMeterAddr(p.CommUid) + rtuCommUid := p.CommUid + var mp deviceManagement.MeterPower + totalEnergy, aEnergy, bEnergy, cEnergy, err := mc.ReadEnergy(rtuCommUid, addr) + if err != nil { + panic(err) + } + mp.TotalActiveEnergy = math.Round(totalEnergy*100) / 100 + mp.AEnergy = math.Round(aEnergy*100) / 100 + mp.BEnergy = math.Round(bEnergy*100) / 100 + mp.CEnergy = math.Round(cEnergy*100) / 100 + + _, aVoltage, bVoltage, cVoltage, err := mc.ReadVoltage(rtuCommUid, addr) + if err != nil { + panic(err) + } + mp.AVoltage = math.Round(aVoltage*100) / 100 + mp.BVoltage = math.Round(bVoltage*100) / 100 + mp.CVoltage = math.Round(cVoltage*100) / 100 + + _, aElectricCurrent, bElectricCurrent, cElectricCurrent, err := mc.ReadElectricCurrent(rtuCommUid, addr) + if err != nil { + panic(err) + } + mp.AElectricCurrent = math.Round(aElectricCurrent*100) / 100 + mp.BElectricCurrent = math.Round(bElectricCurrent*100) / 100 + mp.CElectricCurrent = math.Round(cElectricCurrent*100) / 100 + + totalPower, aPower, bPower, cPower, err := mc.ReadPower(rtuCommUid, addr) + if err != nil { + panic(err) + } + mp.TotalActivePower = math.Round(totalPower*100) / 100 + mp.APower = math.Round(aPower*100) / 100 + mp.BPower = math.Round(bPower*100) / 100 + mp.CPower = math.Round(cPower*100) / 100 + tf, af, bf, cf, err := mc.ReadPowerFactor(rtuCommUid, addr) + if err != nil { + panic(err) + } + mp.TotalPowerFactor = math.Round(tf*1000) / 1000 + mp.APowerFactor = math.Round(af*100) / 100 + mp.BPowerFactor = math.Round(bf*100) / 100 + mp.CPowerFactor = math.Round(cf*100) / 100 + if mp.TotalPowerFactor >= 1 { + mp.TotalPowerFactor = 0.999 + } + rpc.WriteResult(mp) +} + +func getMsgAddress(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + } + rpc.ReadParams(&p) + addr, err := mc.ReadMsgAddr(p.CommUid, getMeterAddr(p.CommUid)) + if err != nil { + panic(err) + } + rpc.WriteResult(fmt.Sprintf("0x%X", addr)) +} + +// 获取日冻结时间 +func getDailyFreezingTime(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + } + rpc.ReadParams(&p) + + time, err := mc.ReadDailyFreezingTime(p.CommUid, getMeterAddr(p.CommUid)) + if err != nil { + panic(terror.Wrap(err, "getDailyFreezingTime")) + } + + rpc.WriteResult(time) + +} +func geDeviceTime(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + } + rpc.ReadParams(&p) + + time, err := mc.ReadTime(p.CommUid, getMeterAddr(p.CommUid)) + if err != nil { + panic(terror.Wrap(err, "geTime")) + } + rpc.WriteResult(time.Format("2006-01-02 15:04:05")) + +} + +func getReadMonthlyFreezingTime(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + } + rpc.ReadParams(&p) + + day, hour, err := mc.ReadMonthlyFreezingTime(p.CommUid, getMeterAddr(p.CommUid)) + if err != nil { + panic(terror.Wrap(err, "getReadMonthlyFreezingTime")) + } + r := map[string]interface{}{} + r["day"] = day + r["hour"] = hour + rpc.WriteResult(r) +} +func getFreezeTheData(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + } + rpc.ReadParams(&p) + + /*read, err := ADL400.H6000.Read(p.CommUid, getMeterAddr(p.CommUid)) + if err != nil { + panic(err) + } + block, err := ParseHistoricalEnergyBlock(read) + if err != nil { + panic(err) + }*/ + dayData := []*HistoricalEnergyBlock{} + monthData := []*HistoricalEnergyBlock{} + for i := 1; i <= 90; i++ { + time.Sleep(10 * time.Second) + + address, err := ADL400.GetDailyBlockAddress(i) + if err != nil { + panic(err) + } + g.Log().Debugf("GetDailyBlockAddress:% X", address) + read, err := address.Read(p.CommUid, getMeterAddr(p.CommUid)) + if err != nil { + g.Log().Error(err) + continue + } + block, err := ParseHistoricalEnergyBlock(read) + if err != nil { + panic(err) + } + dayData = append(dayData, block) + } + for i := 1; i <= 48; i++ { + time.Sleep(10 * time.Second) + address, err := ADL400.GetMonthlyBlockAddress(i) + if err != nil { + g.Log().Error(err) + continue + + } + g.Log().Debugf("GetMonthlyBlockAddress:% X", address) + read, err := address.Read(p.CommUid, getMeterAddr(p.CommUid)) + if err != nil { + g.Log().Error(err) + continue + } + block, err := ParseHistoricalEnergyBlock(read) + if err != nil { + panic(err) + } + monthData = append(monthData, block) + } + r := map[string]interface{}{} + r["day"] = dayData + r["month"] = monthData + rpc.WriteResult(r) +} + +// HistoricalEnergyBlock 包含块读取的历史电能数据 +type HistoricalEnergyBlock struct { + FreezeTime time.Time // 冻结时间 + TotalActiveEnergy float64 // 总有功电能 (kWh) + PeakActiveEnergy float64 // 有功尖电能 (kWh) + FlatActiveEnergy float64 // 有功峰电能 (kWh) + OffPeakActiveEnergy float64 // 有功平电能 (kWh) + ValleyActiveEnergy float64 // 有功谷电能 (kWh) + TotalReactiveEnergy float64 // 总无功电能 (kvarh) + PeakReactiveEnergy float64 // 无功尖电能 (kvarh) + FlatReactiveEnergy float64 // 无功峰电能 (kvarh) + OffPeakReactiveEnergy float64 // 无功平电能 (kvarh) + ValleyReactiveEnergy float64 // 无功谷电能 (kvarh) + PhaseAActiveEnergy float64 // A相正向有功电能 (kWh) + PhaseBActiveEnergy float64 // B相正向有功电能 (kWh) + PhaseCActiveEnergy float64 // C相正向有功电能 (kWh) + MaxActiveDemand float64 // 有功最大需量 (kW) + MaxActiveDemandTime time.Time // 有功最大需量发生时间 + MaxReactiveDemand float64 // 无功最大需量 (kvar) + MaxReactiveDemandTime time.Time // 无功最大需量发生时间 +} + +// parseUint16Time 解析寄存器中的时间格式 +func parseUint16Time(register uint16) (byte, byte) { + // 寄存器格式:高字节 = 第一部分,低字节 = 第二部分 + return byte(register >> 8), byte(register & 0xFF) +} + +// parseEnergy 解析电能值 (32位数据:2个寄存器) +func parseEnergy(registers []uint16, offset int) float64 { + value := uint32(registers[offset])<<16 | uint32(registers[offset+1]) + // 假设精度为0.01 kWh/kvarh (根据实际情况调整) + return float64(value) * 0.01 +} + +// parseDemand 解析需量值 (16位数据) +func parseDemand(register uint16) float64 { + // 假设精度为0.01 kW/kvar (根据实际情况调整) + return float64(register) * 0.01 +} + +// parseDateTime 解析日期时间 (年、月、日、时、分) +func parseDateTime(year, month, day, hour, minute uint16) time.Time { + // 处理特殊年份格式 (假设2000年基准) + if year < 100 { + year += 2000 + } + + // 注意:冻结日期可能不是当前日期 + return time.Date( + int(year), + time.Month(month), + int(day), + int(hour), + int(minute), + 0, 0, time.Local, + ) +} + +// ParseHistoricalEnergyBlock 解析 H6000 块读取的数据 +func ParseHistoricalEnergyBlock(registers []uint16) (*HistoricalEnergyBlock, error) { + // 验证数据长度应为 34个寄存器 * 2字节/寄存器 = 68字节 + if len(registers) != 34 { + return nil, fmt.Errorf("invalid data length: expected 68 bytes, got %d", len(registers)) + } + + // 将字节数据转换为 uint16 寄存器数组 + /* registers := make([]uint16, 34) + err := binary.Read(bytes.NewReader(data), binary.BigEndian, ®isters) + if err != nil { + return nil, fmt.Errorf("binary read error: %v", err) + }*/ + + block := &HistoricalEnergyBlock{} + + // 解析冻结时间 + freezeYear, freezeMonth := parseUint16Time(registers[0]) // 6000H: 年-月 + freezeDay, freezeHour := parseUint16Time(registers[1]) // 6001H: 日-时 + block.FreezeTime = parseDateTime( + uint16(freezeYear), uint16(freezeMonth), + uint16(freezeDay), uint16(freezeHour), 0, + ) + + // 解析电能数据 (32位值, 各2个寄存器) + block.TotalActiveEnergy = parseEnergy(registers, 2) // 6002H-6003H + block.PeakActiveEnergy = parseEnergy(registers, 4) // 6004H-6005H + block.FlatActiveEnergy = parseEnergy(registers, 6) // 6006H-6007H (峰) + block.OffPeakActiveEnergy = parseEnergy(registers, 8) // 6008H-6009H (平) + block.ValleyActiveEnergy = parseEnergy(registers, 10) // 600AH-600BH (谷) + + block.TotalReactiveEnergy = parseEnergy(registers, 12) // 600CH-600DH + block.PeakReactiveEnergy = parseEnergy(registers, 14) // 600EH-600FH + block.FlatReactiveEnergy = parseEnergy(registers, 16) // 6010H-6011H (峰) + block.OffPeakReactiveEnergy = parseEnergy(registers, 18) // 6012H-6013H (平) + block.ValleyReactiveEnergy = parseEnergy(registers, 20) // 6014H-6015H (谷) + + block.PhaseAActiveEnergy = parseEnergy(registers, 22) // 6016H-6017H + block.PhaseBActiveEnergy = parseEnergy(registers, 24) // 6018H-6019H + block.PhaseCActiveEnergy = parseEnergy(registers, 26) // 601AH-601BH + + // 解析有功最大需量及时间 + block.MaxActiveDemand = parseDemand(registers[28]) // 601CH (有功最大需量) + + minute, hour := parseUint16Time(registers[29]) // 601DH: 分-时 + day, month := parseUint16Time(registers[30]) // 601EH: 日-月 + block.MaxActiveDemandTime = parseDateTime( + uint16(block.FreezeTime.Year()), uint16(month), + uint16(day), uint16(hour), uint16(minute), + ) + + // 解析无功最大需量及时间 + block.MaxReactiveDemand = parseDemand(registers[31]) // 601FH (无功最大需量) + + minute, hour = parseUint16Time(registers[32]) // 6020H: 分-时 + day, month = parseUint16Time(registers[33]) // 6021H: 日-月 + block.MaxReactiveDemandTime = parseDateTime( + uint16(block.FreezeTime.Year()), uint16(month), + uint16(day), uint16(hour), uint16(minute), + ) + + return block, nil +} diff --git a/internal/module/adl400TtyApi/write.go b/internal/module/adl400TtyApi/write.go new file mode 100644 index 0000000..933aeb9 --- /dev/null +++ b/internal/module/adl400TtyApi/write.go @@ -0,0 +1,23 @@ +package adl400TtyApi + +import ( + "github.com/gogf/gf/v2/util/gconv" + "github.com/towgo/towgo/towgo" + "strings" +) + +func setMsgAddress(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + MsgAddr string `json:"msg_addr"` + } + rpc.ReadParams(&p) + + oldAddr := getMeterAddr(p.CommUid) + newAddr := gconv.Byte(strings.TrimPrefix(p.MsgAddr, "0x")) + err := mc.WriteMsgAddr(p.CommUid, oldAddr, newAddr) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} diff --git a/internal/module/basicInfo/deviceManagement/meter.go b/internal/module/basicInfo/deviceManagement/meter.go new file mode 100644 index 0000000..5c76d14 --- /dev/null +++ b/internal/module/basicInfo/deviceManagement/meter.go @@ -0,0 +1,46 @@ +package deviceManagement + +import ( + "tgk-touch/internal/library/meterControl" +) + +type MeterPower struct { + MeterId int64 `json:"meterId" xorm:"comment('电表ID')"` + MeterAddr string `json:"meterAddr" xorm:"comment('电表从地址')"` + + AVoltage float64 `json:"aVoltage" xorm:"comment('A相电压V/单相电压')"` + AElectricCurrent float64 `json:"aElectricCurrent" xorm:"comment('A相电流A/单相电流')"` + APower float64 `json:"aPower" xorm:"comment('A相功率Kw/单相功率')"` + AEnergy float64 `json:"aEnergy" xorm:"comment('A相电能Kwh/单相电能')"` + APowerFactor float64 `json:"aPowerFactor" xorm:"comment('A相功率因数/单相功率因数')"` + + BVoltage float64 `json:"bVoltage" xorm:"comment('B相电压')"` + BElectricCurrent float64 `json:"bElectricCurrent" xorm:"comment('B相电流')"` + BPower float64 `json:"bPower" xorm:"comment('B相功率')"` + BEnergy float64 `json:"bEnergy" xorm:"comment('B相电能Kwh')"` + BPowerFactor float64 `json:"bPowerFactor" xorm:"comment('B相功率因数')"` + + CVoltage float64 `json:"cVoltage" xorm:"comment('C相电压')"` + CElectricCurrent float64 `json:"cElectricCurrent" xorm:"comment('C相电流')"` + CPower float64 `json:"cPower" xorm:"comment('C相功率')"` + CEnergy float64 `json:"cEnergy" xorm:"comment('C相电能Kwh')"` + CPowerFactor float64 `json:"cPowerFactor" xorm:"comment('C相功率因数')"` + + TotalActivePower float64 `json:"activePower" xorm:"'total_active_power' comment('总有功功率Kw')"` // 006AH + TotalActiveEnergy float64 `json:"activeEnergy" xorm:"'total_active_energy' comment('总有功电能Kwh')"` // 000A + TotalPowerFactor float64 `json:"powerFactor" xorm:"'total_power_factor' comment('总功率因数')"` +} + +type Meter struct { + CommUid string `json:"commUid"` + RtuId int64 `json:"rtuId" xorm:"comment('所属监控终端Id')"` + RoadId int64 `json:"roadId" xorm:"comment('路段Id')"` + ActiveTime string `json:"activeTime" xorm:"comment('激活时间')"` + MeterName string `json:"meterName" xorm:"comment('电表名称')"` + MeterAddress string `json:"meterAddress" xorm:"comment('电表从地址')"` + Phase3 bool `json:"phase3" xorm:"comment('相位 false单相 true三项')"` + MeterModel meterControl.MeterFirm `json:"meterModel" xorm:"comment('电表型号')"` + WorkState int64 `json:"workState" xorm:"comment('工作状态 0正常 1禁用 2维修')"` + Description string `json:"description" xorm:"comment('描述')"` + TotalEnergy float64 `json:"total_energy" xorm:"comment('统计能耗')"` +} diff --git a/internal/module/basicInfo/deviceManagement/tgk_main_model.go b/internal/module/basicInfo/deviceManagement/tgk_main_model.go new file mode 100644 index 0000000..b9533ad --- /dev/null +++ b/internal/module/basicInfo/deviceManagement/tgk_main_model.go @@ -0,0 +1,30 @@ +package deviceManagement + +import ( + "tgk-touch/internal/library/m9z" +) + +type ( + TgkMain struct { + Name string `json:"name" xorm:"comment('主控器名称')" ` + RtuId int64 `json:"rtuId" xorm:"comment('终端ID')" ` + OpenLoopCount int `json:"openLoopCount" xorm:"comment('开启回路数')"` + Mode int `json:"mode" xorm:"comment('1~2自动模式, -1 手动模式')"` + UpgradeProgress uint32 `json:"upgrade_progress" xorm:"comment('升级进度地址')"` + UpgradeStatus int `json:"upgrade_status" xorm:"comment('升级进度 0 未完成 1完成')"` + UpgradeFirmwareId string `json:"upgrade_firmware_id" xorm:"comment('固件包升级id')"` + FirmwareVersion string `json:"firmware_version" xorm:"comment('固件版本')"` + + BleEnable bool `json:"ble_enable" xorm:"comment('是否启用蓝牙超时')"` + BleOfflineMin uint8 `json:"ble_offline_min" xorm:"comment('蓝牙超时时间')"` + BleName string `json:"ble_name" xorm:"comment('蓝牙名称')"` + Temperature float64 `json:"temperature" xorm:"comment('温度')"` + Loops []*TgkLoop `json:"loops" xorm:"json"` + } + TgkLoop struct { + m9z.Loop + V float32 `json:"v"` + A float32 `json:"a"` + Kw float32 `json:"kw"` + } +) diff --git a/internal/module/license_terminal/license.model.go b/internal/module/license_terminal/license.model.go new file mode 100644 index 0000000..becf812 --- /dev/null +++ b/internal/module/license_terminal/license.model.go @@ -0,0 +1,26 @@ +package licenseterminal + +type License struct { + ID int64 `json:"id"` //Licenseid + LicenseKey string `json:"license_key"` //许可证密钥 + AccessCode string `json:"access_code"` //授权码 + AccessCodeVersion string `json:"access_code_version"` //授权码的版本 + OrderSerialNumber string `json:"order_serial_number"` //订单编号 + ProductSerialNumber string `json:"product_serial_number"` //产品序列号 + ProductNumber string `json:"product_number"` //产品编号 + ProductName string `json:"product_name"` //产品名称 + Starttime int64 `json:"starttime"` //起始日 + Endtime int64 `json:"endtime"` //截止日期 + Sale string `json:"sale"` //销售人员 + CustomerName string `json:"customer_name"` //客户名称 + Remark string `json:"remark"` //备注 + Valid bool `json:"valid"` //是否有效(非存储结构) + ValidErrMessage string `json:"valid_err_message"` //无效的错误信息 +} + +type ActiveRequestInfo struct { + ProductNumber string `json:"product_number"` + AccessCode string `json:"access_code"` + AccessCodeVersion string `json:"access_code_version"` + LicenseKey string `json:"license_key"` +} diff --git a/internal/module/license_terminal/licenseController.go b/internal/module/license_terminal/licenseController.go new file mode 100644 index 0000000..fc68b90 --- /dev/null +++ b/internal/module/license_terminal/licenseController.go @@ -0,0 +1,15 @@ +package licenseterminal + +import ( + "github.com/towgo/towgo/towgo" +) + +var method string = "/license/terminal" + +func init() { + towgo.SetFunc(method+"/list", list) +} + +func list(rpcConn towgo.JsonRpcConnection) { + rpcConn.WriteResult(GetLicense()) +} diff --git a/internal/module/license_terminal/license_terminal.package.go b/internal/module/license_terminal/license_terminal.package.go new file mode 100644 index 0000000..3a5b64a --- /dev/null +++ b/internal/module/license_terminal/license_terminal.package.go @@ -0,0 +1,820 @@ +package licenseterminal + +import ( + "encoding/json" + "fmt" + "github.com/gogf/gf/v2/util/gconv" + "github.com/towgo/towgo/errors/terror" + "io" + "log" + "net" + "net/http" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strings" + "sync" + "tgk-touch/internal/global" + "time" + + "github.com/towgo/towgo/lib/system" + "github.com/towgo/towgo/towgo" +) + +var accessCode string +var expirationCallFuncs sync.Map //存放到期回调的注册函数 +var activeCallFuncs sync.Map //存放激活回调的函数 +var isChecking bool //自动检查是否运行 +var pathSymbol string = system.GetPathSymbol() +var basePath string = system.GetPathOfProgram() +var serverPath string = filepath.Join(basePath, "config", "license.conf.json") +var productNumber string + +func init() { + + towgo.SetFunc("/license/list", func(rpcConn towgo.JsonRpcConnection) { + result := map[string]interface{}{} + result["rows"] = GetLicense() + rpcConn.WriteResult(result) + }) + + //获取激活码 + towgo.SetFunc("/license/activecode/get", func(rpcConn towgo.JsonRpcConnection) { + + err := activecodeRequestCheck(rpcConn) + if err != nil { + rpcConn.GetRpcResponse().Error.Set(towgo.JSONRPC_500_INTERNAL_SERVER_ERROR, err.Error()) + rpcConn.Write() + return + } + + var requestParams struct { + ProductNumber string `json:"product_number"` + LicenseKey string `json:"license_key"` + AccessCode string `json:"access_code"` + } + rpcConn.ReadParams(&requestParams) + + if requestParams.ProductNumber == "" { + requestParams.ProductNumber = productNumber + } + + var active ActiveRequestInfo + + if requestParams.AccessCode != "" { + active.AccessCode = requestParams.AccessCode + } else { + active.AccessCode = GetAccessCode() + } + + active.LicenseKey = requestParams.LicenseKey + active.ProductNumber = requestParams.ProductNumber + + jb, _ := json.Marshal(active) + b, err := system.EncryptPublic(jb) + if err != nil { + rpcConn.GetRpcResponse().Error.Set(towgo.JSONRPC_500_INTERNAL_SERVER_ERROR, err.Error()) + rpcConn.Write() + return + } + + result := map[string]interface{}{} + result["active_code"] = string(b) + rpcConn.WriteResult(result) + }) + + //联机激活请求 + towgo.SetFunc("/license/activecode/online/request", func(rpcConn towgo.JsonRpcConnection) { + var conf struct { + LicenseServerUrl string + } + system.ScanConfigJson(serverPath, &conf) + + if conf.LicenseServerUrl == "" { + rpcConn.GetRpcResponse().Error.Set(towgo.JSONRPC_500_INTERNAL_SERVER_ERROR, "许可证服务器地址未配置") + rpcConn.Write() + return + } + + err := activecodeRequestCheck(rpcConn) + if err != nil { + rpcConn.GetRpcResponse().Error.Set(towgo.JSONRPC_500_INTERNAL_SERVER_ERROR, err.Error()) + rpcConn.Write() + return + } + + var requestParams struct { + ProductNumber string `json:"product_number"` + LicenseKey string `json:"license_key"` + } + rpcConn.ReadParams(&requestParams) + + if requestParams.ProductNumber == "" { + requestParams.ProductNumber = productNumber + } + + var active ActiveRequestInfo + active.AccessCode = GetAccessCode() + active.LicenseKey = requestParams.LicenseKey + active.ProductNumber = requestParams.ProductNumber + + jb, _ := json.Marshal(active) + + b, err := system.EncryptPublic(jb) + if err != nil { + rpcConn.GetRpcResponse().Error.Set(towgo.JSONRPC_500_INTERNAL_SERVER_ERROR, err.Error()) + rpcConn.Write() + return + } + + request := towgo.NewJsonrpcrequest() + request.Method = "/license/getActiveCredential" + var activeRequest struct { + ActiveCode string `json:"active_code"` + } + activeRequest.ActiveCode = string(b) + request.Params = activeRequest + + rpcClient := towgo.NewHttpClient() + rpcClient.ErrorFunc = func(err error) { + rpcConn.GetRpcResponse().Error.Set(500, err.Error()) + rpcConn.Write() + } + rpcClient.Call(conf.LicenseServerUrl+"/jsonrpc", request, func(j towgo.Jsonrpcresponse) { + if j.Error.Code != 200 { + rpcConn.GetRpcResponse().Error.Set(j.Error.Code, j.Error.Message) + rpcConn.Write() + return + } + var result struct { + ActiveCredential string `json:"active_credential"` + } + j.ReadResult(&result) + err = SaveLicense(filepath.Join(basePath, "license", requestParams.ProductNumber+".license"), result.ActiveCredential) + if err != nil { + rpcConn.GetRpcResponse().Error.Set(j.Error.Code, err.Error()) + rpcConn.Write() + return + } + InstallReNewCheck() + l, err := GetLicenseByName(requestParams.ProductNumber) + if err != nil { + rpcConn.GetRpcResponse().Error.Set(towgo.JSONRPC_500_INTERNAL_SERVER_ERROR, err.Error()) + rpcConn.Write() + return + } + rpcConn.WriteResult(l) + }) + + }) + + towgo.SetFunc("/license/active", func(rpcConn towgo.JsonRpcConnection) { + + var params struct { + ActiveCredential string `json:"active_credential"` + } + rpcConn.ReadParams(¶ms) + + l, err := DecodeLicense(params.ActiveCredential) + if err != nil { + rpcConn.GetRpcResponse().Error.Set(500, err.Error()) + rpcConn.Write() + return + } + err = SaveLicense(filepath.Join(basePath, "license", l.OrderSerialNumber+".license"), params.ActiveCredential) + if err != nil { + rpcConn.GetRpcResponse().Error.Set(500, err.Error()) + rpcConn.Write() + return + } + InstallReNewCheck() + rpcConn.WriteResult("ok") + }) + + towgo.SetFunc("/license/accesscode/get", func(rpcConn towgo.JsonRpcConnection) { + result := map[string]interface{}{} + result["accesscode"] = GetAccessCode() + rpcConn.WriteResult(result) + }) + + //获取授权码 + http.HandleFunc("/license/accesscode/get", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte(GetAccessCode())) + }) + //安装许可证 + http.HandleFunc("/license/install/renew", func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("成功安装了" + gconv.String(InstallReNewCheck()) + "个许可证")) + }) + accessCode = GetAccessCode() + //go hardCheckExpiration() + + go autoCheckExpiration() +} + +// 定时任务 检查许可证过期情况 +func autoCheckExpiration() { + + if isChecking { + return + } + for { + isChecking = true + time.Sleep(time.Second * 60) + log.Println("检查许可证") + //注册函数检查 + licenses := GetLicense() + expirationCallFuncs.Range(func(key, _ any) bool { + + //许可证查询 + for _, v := range licenses { + if v.AccessCode != accessCode { + log.Print("许可证与授权码不匹配") + continue + } + //许可证存在 + if v.ProductNumber == key.(string) { + //到期检查 + + if v.Endtime > time.Now().UnixMilli() { + //没有过期 + return true + } + + } + } + + //许可证不存在或过期 + notifyExpiration(key.(string)) + //继续迭代 + return true + }) + } +} + +func activecodeRequestCheck(rpcConn towgo.JsonRpcConnection) error { + var requestParams struct { + ProductNumber string `json:"product_number"` + LicenseKey string `json:"license_key"` + } + + rpcConn.ReadParams(&requestParams) + if requestParams.LicenseKey == "" { + return terror.New("license_key can not be null") + } + + if requestParams.ProductNumber == "" { + requestParams.ProductNumber = productNumber + } + + licenseKeys := strings.Split(requestParams.LicenseKey, "-") + if len(licenseKeys) != 4 { + return terror.New("license_key error") + } + + if strings.ToUpper(cehckSumString([]byte(requestParams.ProductNumber))) != licenseKeys[0] { + return terror.New("license_key not support for product number") + } + + check := licenseKeys[0] + "-" + licenseKeys[1] + "-" + licenseKeys[2] + check = strings.ToUpper(check) + crcStr := strings.ToUpper(cehckSumString([]byte(check))) + if crcStr != licenseKeys[3] { + return terror.New("license_key error") + } + return nil +} + +// 到期通知回调程序(不传值代表广播,即代表所有的授权都过期了) +func notifyExpiration(ProductSerialNumber string) { + log.Println("notifyExpiration 许可证全过期了") + if ProductSerialNumber == "" { + expirationCallFuncs.Range(func(_, value any) bool { + value.(func())() + return true + }) + return + } + + value, ok := expirationCallFuncs.Load(ProductSerialNumber) + if ok { + value.(func())() + } +} + +func getActiveRequestInfoCode(licenseKey, productNumber string) string { + var active ActiveRequestInfo + active.AccessCode = GetAccessCode() + active.LicenseKey = licenseKey + active.ProductNumber = productNumber + jb, _ := json.Marshal(active) + b, err := system.EncryptPublic(jb) + if err != nil { + return err.Error() + } + return string(b) +} + +// 安装许可证 +func notifyReNew(ProductSerialNumber string) { + if ProductSerialNumber == "" { + activeCallFuncs.Range(func(_, value any) bool { + value.(func())() + return true + }) + return + } + value, ok := activeCallFuncs.Load(ProductSerialNumber) + if ok { + value.(func())() + } +} + +func CheckLicense(l License) error { + if l.AccessCode != accessCode { + return terror.New("许可证与授权码不匹配") + } + if l.Endtime < time.Now().UnixMilli() { + return terror.New("许可证到期") + } + return nil +} + +func InstallReNewCheck() int64 { + licenses := GetLicense() + var installCount int64 = 0 + for _, v := range licenses { + if v.AccessCode != accessCode { + log.Print("许可证与授权码不匹配") + continue + } + + var isExp bool = false + if v.Endtime < time.Now().UnixMilli() { + isExp = true + } + // 通知 + if !isExp { + installCount++ + notifyReNew(v.ProductNumber) + } + } + return installCount +} + +// 授权码计算逻辑 +func GetAccessCode() string { + accesscode := GetMac() + hostname, _ := os.Hostname() + accesscode = accesscode + hostname + accesscode = system.MD5(accesscode) + return accesscode +} + +func GetAccessCode_V2() string { + accesscode := GetMac() + hostname, _ := os.Hostname() + accesscode = accesscode + hostname + accesscode = system.MD5(accesscode) + return accesscode +} + +// 注册超过有效期回调 +func RegExpirationCall(ProductSerialNumber string, callbackfunc func()) { + expirationCallFuncs.Store(ProductSerialNumber, callbackfunc) +} + +// 产品续存回调 +func RegReNewCall(ProductSerialNumber string, callbackfunc func()) { + activeCallFuncs.Store(ProductSerialNumber, callbackfunc) +} + +func GetMac() string { + goos := runtime.GOOS + netInterfaces, err := net.Interfaces() + if err != nil { + fmt.Printf("fail to get net interfaces: %v\n", err) + } + sort.Slice(netInterfaces, func(i, j int) bool { + return netInterfaces[i].Index < netInterfaces[j].Index + }) + switch goos { + case "windows": + for _, netInterface := range netInterfaces { + macAddr := netInterface.HardwareAddr.String() + if len(macAddr) == 0 { + continue + } + if strings.HasPrefix(strings.ToLower(netInterface.Name), "以太网") { + return macAddr + } + } + for _, netInterface := range netInterfaces { + macAddr := netInterface.HardwareAddr.String() + if len(macAddr) == 0 { + continue + } + + if strings.HasPrefix(strings.ToLower(netInterface.Name), "eth") { + return macAddr + } + } + for _, netInterface := range netInterfaces { + macAddr := netInterface.HardwareAddr.String() + if len(macAddr) == 0 { + continue + } + if strings.HasPrefix(strings.ToLower(netInterface.Name), "wl") { + return macAddr + } + } + case "linux": + for _, netInterface := range netInterfaces { + macAddr := netInterface.HardwareAddr.String() + if len(macAddr) == 0 { + continue + } + if strings.HasPrefix(strings.ToLower(netInterface.Name), "en") { + return macAddr + } + } + for _, netInterface := range netInterfaces { + macAddr := netInterface.HardwareAddr.String() + if len(macAddr) == 0 { + continue + } + if strings.HasPrefix(strings.ToLower(netInterface.Name), "et") { + return macAddr + } + } + for _, netInterface := range netInterfaces { + macAddr := netInterface.HardwareAddr.String() + if len(macAddr) == 0 { + continue + } + if strings.HasPrefix(strings.ToLower(netInterface.Name), "wla") { + return macAddr + } + } + default: + return "" + } + return "" +} + +func GetLicense() []License { + list, _ := ListDir(filepath.Join(basePath, "license"), "license") + + var listLicenses []License + var id int64 = 1 + for _, v := range list { + l, err := OpenLicense(v) + + if err != nil { + log.Print(err.Error()) + continue + } + err = CheckLicense(l) + if err != nil { + l.ValidErrMessage = err.Error() + l.Valid = false + } else { + l.Valid = true + } + l.ID = id + id++ + listLicenses = append(listLicenses, l) + } + return listLicenses +} + +func GetLicenseByProductNumber(productNumber string) []License { + list, _ := ListDir(filepath.Join(basePath, "license"), "license") + + var listLicenses []License + for _, v := range list { + l, err := OpenLicense(v) + + if err != nil { + log.Print(err.Error()) + continue + } + + if l.ProductNumber != productNumber { + continue + } + + err = CheckLicense(l) + if err != nil { + l.ValidErrMessage = err.Error() + l.Valid = false + } else { + l.Valid = true + } + listLicenses = append(listLicenses, l) + } + return listLicenses +} + +func GetLicenseByName(name string) (License, error) { + l, err := OpenLicense(filepath.Join(basePath, "license", name+".license")) + if err != nil { + return l, err + } + err = CheckLicense(l) + if err != nil { + l.ValidErrMessage = err.Error() + l.Valid = false + } else { + l.Valid = true + } + return l, err +} + +func QueryLicense(name string) (License, error) { + list, _ := ListDir(filepath.Join(basePath, "license"), "license") + for _, v := range list { + _, fileName := filepath.Split(v) + if name+".license" == fileName { + return OpenLicense(v) + } + } + var l License + return l, terror.New("license not found") +} + +func OpenLicense(path string) (License, error) { + //只读打开 + var l License + file, err := os.OpenFile(path, os.O_RDONLY, 0644) + if err != nil { + return l, err + } + defer file.Close() + b, err := io.ReadAll(file) + if err != nil { + return l, err + } + + s, err := system.DecryptPublic(b) + + err = json.Unmarshal([]byte(s), &l) + if err != nil { + return l, err + } + return l, nil +} + +func DecodeLicense(active_credential string) (License, error) { + //只读打开 + var l License + + s, _ := system.DecryptPublic([]byte(active_credential)) + err := json.Unmarshal([]byte(s), &l) + if err != nil { + return l, err + } + return l, nil +} + +// 自动激活 +func AutoFristActive() { + + var ( + licenseKey string = g.Config().LicenseConf.LicenseKey + licenseServerUrl string = g.Config().LicenseConf.LicenseServerUrl + ) + + //system.ScanConfigJson(serverPath, &conf) + if licenseKey == "" { + return + } + licenses := GetLicense() + for _, v := range licenses { + if v.LicenseKey == licenseKey { + if v.Valid { + //许可证存在并且有效即不需要激活 + return + } + } + } + var active ActiveRequestInfo + active.AccessCode = GetAccessCode() + active.LicenseKey = licenseKey + active.ProductNumber = productNumber + + jb, _ := json.Marshal(active) + + b, err := system.EncryptPublic(jb) + if err != nil { + log.Print(err.Error()) + return + } + + request := towgo.NewJsonrpcrequest() + request.Method = "/license/getActiveCredential" + var activeRequest struct { + ActiveCode string `json:"active_code"` + } + activeRequest.ActiveCode = string(b) + request.Params = activeRequest + rpcClient := towgo.NewHttpClient() + rpcClient.ErrorFunc = func(err error) { + log.Print(err.Error()) + } + rpcClient.Call(licenseServerUrl+"/jsonrpc", request, func(j towgo.Jsonrpcresponse) { + if j.Error.Code != 200 { + log.Print("请求授权失败 ", j.Error.Message) + return + } + var result struct { + ActiveCredential string `json:"active_credential"` + } + j.ReadResult(&result) + err = SaveLicense(filepath.Join(basePath, "license", productNumber+".license"), result.ActiveCredential) + if err != nil { + log.Printf("SaveLicense err %+v", err) + return + } + InstallReNewCheck() + _, err = GetLicenseByName(productNumber) + if err != nil { + log.Print(err) + + return + } + }) + +} + +func SaveLicense(savePath string, active string) error { + // 1. 确保目录存在 + dir := filepath.Dir(savePath) // 提取目录路径(如 "license") + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + // 2. 创建文件并写入内容 + file, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + defer file.Close() + + _, err = file.WriteString(active) + return err + +} + +// 获取指定目录下的所有文件,不进入下一级目录搜索,可以匹配后缀过滤。 +func ListDir(dirPth string, suffix string) (files []string, err error) { + files = make([]string, 0, 10) + + dir, err := os.ReadDir(dirPth) + if err != nil { + return nil, err + } + + PthSep := string(os.PathSeparator) + suffix = strings.ToUpper(suffix) //忽略后缀匹配的大小写 + + for _, fi := range dir { + if fi.IsDir() { // 忽略目录 + continue + } + if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) { //匹配文件 + files = append(files, dirPth+PthSep+fi.Name()) + } + } + return files, nil +} + +// 获取指定目录及所有子目录下的所有文件,可以匹配后缀过滤。 +func WalkDir(dirPth, suffix string) (files []string, err error) { + files = make([]string, 0, 30) + suffix = strings.ToUpper(suffix) //忽略后缀匹配的大小写 + + err = filepath.Walk(dirPth, func(filename string, fi os.FileInfo, _ error) error { //遍历目录 + + if fi.IsDir() { // 忽略目录 + return nil + } + + if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) { + files = append(files, filename) + } + + return nil + }) + + return files, err +} + +var mbTable = []uint16{ + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, + 0x8c48, 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, + 0x1081, 0x0108, 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, + 0x9cc9, 0x8d40, 0xbfdb, 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, + 0x2102, 0x308b, 0x0210, 0x1399, 0x6726, 0x76af, 0x4434, 0x55bd, + 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, 0xfae7, 0xc87c, 0xd9f5, + 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, 0x54b5, 0x453c, + 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, 0xc974, + 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, + 0x5285, 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, + 0xdecd, 0xcf44, 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, + 0x6306, 0x728f, 0x4014, 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, + 0xef4e, 0xfec7, 0xcc5c, 0xddd5, 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, + 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, 0x242a, 0x16b1, 0x0738, + 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, 0x9af9, 0x8b70, + 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, 0xf0b7, + 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, + 0x18c1, 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, + 0xa50a, 0xb483, 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, + 0x2942, 0x38cb, 0x0a50, 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, + 0xb58b, 0xa402, 0x9699, 0x8710, 0xf3af, 0xe226, 0xd0bd, 0xc134, + 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, 0x6e6e, 0x5cf5, 0x4d7c, + 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, 0xa33a, 0xb2b3, + 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, 0x3efb, + 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, + 0xe70e, 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, + 0x6b46, 0x7acf, 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, + 0xf78f, 0xe606, 0xd49d, 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, + 0x7bc7, 0x6a4e, 0x58d5, 0x495c, 0x3de3, 0x2c6a, 0x1ef1, 0x0f78, +} + +func checkSum(data []byte) uint16 { + var crc16 uint16 + crc16 = 0x0000 + for _, v := range data { + n := uint8(uint16(v) ^ crc16) + crc16 >>= 8 + crc16 ^= mbTable[n] + } + return crc16 +} + +func cehckSumString(data []byte) string { + str := gconv.String(checkSum(data)) + stLen := len(str) + for i := 0; i < 4-stLen; i++ { + str = "0" + str + } + return str +} + +func SetProductNumber(p string) { + productNumber = p +} + +func getUUID() (string, error) { + var cmd *exec.Cmd + platform := strings.ToLower(osName()) + switch platform { + case "windows": + cmd = exec.Command("wmic", "csproduct", "get", "UUID") + case "linux": + cmd = exec.Command("cat", "/etc/machine-id") + case "darwin": + cmd = exec.Command("ioreg", "-rd1", "-c", "IOPlatformExpertDevice") + default: + return "", fmt.Errorf("unsupported platform: %s", platform) + } + + out, err := cmd.Output() + if err != nil { + return "", err + } + + uuid := parseUUID(string(out), platform) + return uuid, nil +} + +func osName() string { + return fmt.Sprintf("%s", strings.ToLower(runtime.GOOS)) +} + +func parseUUID(input, platform string) string { + switch platform { + case "windows": + lines := strings.Split(input, "\n") + for _, line := range lines { + if strings.Contains(line, "UUID") { + return strings.TrimSpace(strings.Replace(line, "UUID", "", 1)) + } + } + case "linux": + return strings.TrimSpace(input) + case "darwin": + lines := strings.Split(input, "\n") + for _, line := range lines { + if strings.Contains(line, "IOPlatformUUID") { + return strings.TrimSpace(strings.Split(line, "=")[1]) + } + } + } + return "" +} diff --git a/internal/module/m9zTtyApi/cron.go b/internal/module/m9zTtyApi/cron.go new file mode 100644 index 0000000..ba725b7 --- /dev/null +++ b/internal/module/m9zTtyApi/cron.go @@ -0,0 +1,175 @@ +package m9zApi + +import ( + "encoding/json" + "fmt" + "github.com/towgo/towgo/errors/terror" + "github.com/towgo/towgo/lib/system" + "os" + "path/filepath" + "sort" + "sync" + "tgk-touch/internal/global" + "tgk-touch/internal/library/m9z" + "time" +) + +var l sync.Mutex +var filePath = system.GetPathOfProgram() + "data/data.json" // JSON 文件路径 + +const ( + maxItems = 50 // 最大存储条数 +) + +func ScanM9z() { + + tryLock := l.TryLock() + if !tryLock { + return + } + defer l.Unlock() + /** 挨个查询是否有报警*/ + cuid := deviceInfo["deviceId"].(string) + for i := 0; i < 10; i++ { + read, err := m9z.SubLoopParametersRead(cuid, uint(i)) + if err != nil { + g.Log().Error("sub loop parameters read error:", err) + continue + } + if read == nil { + continue + } + if read.AlarmStatus != 0 { + var mph M9zFaultHistory + mph.CommUid = cuid + mph.LoopIdx = uint(i) + 1 + mph.FaultCode = read.AlarmCode + mph.Voltage = read.DCVoltage + mph.FaultTime = time.Now() + + switch read.AlarmStatus { + case 1: + mph.FaultMsg = "欠压/短路故障" + break + case 2: + mph.FaultMsg = "过压/开路故障" + break + case 8: + mph.FaultMsg = "电源故障" + break + case 9: + mph.FaultMsg = "欠压/电源故障" + break + default: + mph.FaultMsg = "未知异常" + } + err = addData(mph) + if err != nil { + g.Log().Error(terror.Wrap(err, "create error")) + + } + } + } +} + +// 读取现有数据(如果文件不存在则返回空切片) +func readData() ([]M9zFaultHistory, error) { + + if _, err := os.Stat(filePath); os.IsNotExist(err) { + _, err := createFileWithPath(filePath) + if err != nil { + return nil, terror.Wrap(err, "create file error") + } + return []M9zFaultHistory{}, nil // 文件不存在,返回空切片 + } + + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("读取文件失败: %v", err) + } + + var items []M9zFaultHistory + if len(data) > 0 { + if err := json.Unmarshal(data, &items); err != nil { + return nil, fmt.Errorf("解析 JSON 失败: %v", err) + } + } + // 确保ID匹配索引+1(处理旧数据或缺失ID) + for i := range items { + items[i].ID = int64(i + 1) + } + return items, nil +} + +// 写入数据到 JSON 文件 +func writeData(items []M9zFaultHistory) error { + + data, err := json.MarshalIndent(items, "", " ") + if err != nil { + return fmt.Errorf("序列化 JSON 失败: %v", err) + } + return os.WriteFile(filePath, data, 0644) +} + +// 新增数据(自动处理容量限制) +func addData(newItem M9zFaultHistory) error { + // 1. 读取现有数据 + items, err := readData() + if err != nil { + return terror.Wrap(err, "readData") + } + + // 2. 追加新数据 + items = append(items, newItem) + + // 3. 检查容量,如果超过 maxItems 则删除最早的数据(按时间排序) + if len(items) > maxItems { + // 按 Timestamp 排序(最早的在前) + sort.Slice(items, func(i, j int) bool { + return items[i].FaultTime.Before(items[j].FaultTime) + }) + // 删除最早的数据(保留最新的 maxItems 条) + items = items[1:] + for i, _ := range items { + items[i].ID = int64(i + 1) + } + } + + // 4. 写回文件 + err = writeData(items) + + if err != nil { + return terror.Wrap(err, "writeData") + } + + return nil +} + +// 按ID删除数据 +func deleteById(id int) error { + items, err := readData() + if err != nil { + return terror.Wrap(err, "readData") + } + + if id < 1 || id > len(items) { + return fmt.Errorf("ID %d 超出范围", id) + } + + // 删除指定索引的数据(id-1) + items = append(items[:id-1], items[id:]...) + + // 重新分配ID(确保后续数据ID连续) + for i := id - 1; i < len(items); i++ { + items[i].ID = int64(i + 1) + } + return writeData(items) +} +func createFileWithPath(filePath string) (*os.File, error) { + // 创建所有必需的父目录(使用 0755 权限) + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return nil, err + } + // 创建并打开文件(如果已存在则截断) + return os.Create(filePath) +} diff --git a/internal/module/m9zTtyApi/falt.go b/internal/module/m9zTtyApi/falt.go new file mode 100644 index 0000000..cb7d3e2 --- /dev/null +++ b/internal/module/m9zTtyApi/falt.go @@ -0,0 +1,20 @@ +package m9zApi + +import ( + "time" +) + +func (m *M9zFaultHistory) TableName() string { + return `m9z_fault_history` +} + +type M9zFaultHistory struct { + ID int64 `json:"id" xorm:"pk autoincr"` + CommUid string `json:"comm_uid"` + LoopIdx uint `json:"loop_idx"` + Voltage float32 `json:"voltage"` + FaultMsg string `json:"fault_msg"` + FaultCode string `json:"fault_code"` + FaultTime time.Time `json:"fault_time" xorm:"created"` + OperateStatus uint `json:"operate_status" xorm:"default 0 comment('0未处理,1已处理')"` +} diff --git a/internal/module/m9zTtyApi/init.go b/internal/module/m9zTtyApi/init.go new file mode 100644 index 0000000..1e5f0b0 --- /dev/null +++ b/internal/module/m9zTtyApi/init.go @@ -0,0 +1,128 @@ +package m9zApi + +import ( + "encoding/json" + "github.com/towgo/towgo/errors/terror" + "github.com/towgo/towgo/towgo" + "tgk-touch/internal/global" +) + +var deviceInfo map[string]interface{} + +func Init() { + marshal, _ := json.Marshal(g.Config().DeviceInfo) + json.Unmarshal(marshal, &deviceInfo) + towgo.SetFunc("/m9z/fault/list", faultList) + towgo.SetFunc("/m9z/fault/batchExec", execFailByIds) + // 获取设备输出状态参数 + towgo.SetFunc("/m9z/getOutStatus", getOutStatus) + // 获取设备模式 + towgo.SetFunc("/m9z/getDeviceMode", getDeviceMode) + // 设置设备模式 + towgo.SetFunc("/m9z/setDeviceMode", setDeviceMode) + // 获取手动控制信息 + towgo.SetFunc("/m9z/getDeviceManualInfo", getDeviceManualInfo) + // 设置手动控制 + towgo.SetFunc("/m9z/setDeviceManualInfo", setDeviceManualInfo) + // 获取设备时间 + towgo.SetFunc("/m9z/getDeviceTime", getDeviceTime) + // 设置设备时间 + towgo.SetFunc("/m9z/setDeviceTime", setDeviceTime) + // 获取设备经纬度信息 + towgo.SetFunc("/m9z/getLgnLatData", getLgnLatData) + // 设置设备经纬度信息 + towgo.SetFunc("/m9z/setLgnLatData", setLgnLatData) + // 获取日出日落时间偏差 + towgo.SetFunc("/m9z/getSunRiseSet", getSunRiseSet) + // 设置日出日落时间偏差 + towgo.SetFunc("/m9z/setSunRiseSet", setSunRiseSet) + // 获取设备的状态 + towgo.SetFunc("/m9z/getDeviceStatus", getDeviceStatus) + // 实时监控 + towgo.SetFunc("/m9z/getDeviceStatus2", getDeviceStatus2) + // 获取设备的配置 + towgo.SetFunc("/m9z/getDeviceConfig", getDeviceConfig) + // 设置设备的配置 + towgo.SetFunc("/m9z/setDeviceConfig", setDeviceConfig) + // 获取日出日落时间 + towgo.SetFunc("/m9z/getDeviceSunRiseTime", getDeviceSunRiseTime) + // 保存配置参数 + towgo.SetFunc("/m9z/setSave", setSave) + // 重启设备 + towgo.SetFunc("/m9z/setRestart", setRestart) + // 设置经纬度和日出日落时间差 + towgo.SetFunc("/m9z/setDeviceLgnLatAndSunRsTime", setDeviceLgnLatAndSunRsTime) + // 读取开始任务 (优化指令 CMD=0x13) + towgo.SetFunc("/m9z/getStartTask", getStartTask) + // 读取中间任务 (优化指令 CMD=0x14) + towgo.SetFunc("/m9z/getMiddleTaskRead", getMiddleTaskRead) + // 读取结束任务 (优化指令 CMD=0x15) + towgo.SetFunc("/m9z/getStopTaskRead", getStopTaskRead) + // 写入开始任务 (优化指令 CMD=0x13) + towgo.SetFunc("/m9z/setStartTaskWrite", setStartTaskWrite) + + // 写入中间任务 (优化指令 CMD=0x14) + towgo.SetFunc("/m9z/setMiddleTaskWrite", setMiddleTaskWrite) + + // 写入结束任务 (优化指令 CMD=0x15) + towgo.SetFunc("/m9z/setStopTaskWrite", setStopTaskWrite) + towgo.SetFunc("/m9z/getStartTask2", getStartTask2) + towgo.SetFunc("/m9z/getStopTask2", getStopTask2) + towgo.SetFunc("/m9z/setStartTask2", setStartTask2) + towgo.SetFunc("/m9z/setStopTask2", setStopTask2) + towgo.SetFunc("/m9z/getMiddleTask2", getMiddleTask2) + towgo.SetFunc("/m9z/setMiddleTask2", setMiddleTask2) + + towgo.SetFunc("/m9z/getSubLoopParameters", getSubLoopParameters) + towgo.SetFunc("/m9z/getAllSubLoopParameter", getAllSubLoopParameter) + towgo.SetFunc("/m9z/touchdisplay/getDeviceID", getTouchDeviceId) + +} + +func getTouchDeviceId(rpc towgo.JsonRpcConnection) { + + rpc.WriteResult(deviceInfo) +} +func faultDelete(rpc towgo.JsonRpcConnection) { + var p struct { + Id int `json:"id"` + } + rpc.ReadResult(&p) + if p.Id == 0 { + panic(terror.New("id 为空")) + + } + + err := deleteById(p.Id) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} +func faultList(rpc towgo.JsonRpcConnection) { + data, err := readData() + if err != nil { + panic(err) + } + rpc.WriteResult(g.NewPageResult(int64(len(data)), data)) +} + +func execFailByIds(rpc towgo.JsonRpcConnection) { + var p struct { + Ids []int `json:"ids"` + Operate uint `json:"operate"` // 0 处理 1删除 + } + rpc.ReadParams(&p) + if p.Operate == 0 { + + } else { + for _, id := range p.Ids { + err := deleteById(id) + if err != nil { + panic(err) + } + } + } + rpc.WriteResult("ok") + +} diff --git a/internal/module/m9zTtyApi/read.go b/internal/module/m9zTtyApi/read.go new file mode 100644 index 0000000..586c193 --- /dev/null +++ b/internal/module/m9zTtyApi/read.go @@ -0,0 +1,410 @@ +package m9zApi + +import ( + "fmt" + "math" + "sort" + "sync" + "tgk-touch/internal/global" + "tgk-touch/internal/library/m9z" + "time" + + "github.com/towgo/towgo/errors/terror" + "github.com/towgo/towgo/towgo" +) + +func getDeviceId(rpc towgo.JsonRpcConnection) (string, []uint) { + var p struct { + CommUid string `json:"comm_uid"` + Idxs []uint `json:"idxs"` + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + return p.CommUid, p.Idxs +} + +// // 获取设备输出状态参数 +func getOutStatus(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + + read, err := m9z.DeviceOutStatusRead(cuid) + if err != nil { + panic(err) + + } + rpc.WriteResult(read) +} + +// todo 有问题 不管写入什么模式返回都是FF 获取设备编程模式 1-3 对应模式01~模式03 编程模式0~2 +func getDeviceMode(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + + read, err := m9z.ModeRead(cuid) + if err != nil { + panic(err) + } + rpc.WriteResult(read) +} + +// 读取手动控制信息 +func getDeviceManualInfo(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + + manual, err := m9z.ManualRead(cuid) + if err != nil { + panic(err) + } + if !manual.Able { + // 如果当前非手动控制模式就读取设备状态后赋值给手动控制对象 + //读取设备状态 + time.Sleep(time.Second) + read, err := m9z.DeviceStatusRead(cuid) + if err != nil { + panic(err) + } + for i, _ := range manual.Loops { + manual.Loops[i].IsOpen = read.Relays[i] + manual.Loops[i].Pwm = int(read.PWMOutputs[i]) + } + } + rpc.WriteResult(manual) +} + +// 获取设备上的时间 +func getDeviceTime(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + + deviceTime, timestampSec, err := m9z.DeviceTimeRead(cuid) + if err != nil { + panic(err) + } + result := make(map[string]interface{}) + result["deviceTime"] = deviceTime.Format("2006-01-02 15:04:05") + result["timestampSec"] = timestampSec + rpc.WriteResult(result) + +} + +// 获取设备经纬度信息 +func getLgnLatData(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + + manual, err := m9z.LgnLatRead(cuid) + if err != nil { + panic(err) + } + r := map[string]interface{}{} + r["lgn"] = manual.Lgn + r["lat"] = manual.Lat + r["loc"] = manual.Loc.String() + rpc.WriteResult(r) +} + +// 获取日出日落时间偏差 +func getSunRiseSet(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + read, err := m9z.SunRiseSetRead(cuid) + if err != nil { + panic(err) + } + rpc.WriteResult(read) +} + +// 获取设备的状态 +func getDeviceStatus(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + + read, err := m9z.DeviceStatusRead(cuid) + if err != nil { + panic(err) + } + rpc.WriteResult(read) +} + +type DeviceStatusLoop struct { + m9z.Loop + V float32 + A float32 + Kw float32 +} +type DeviceStatus struct { + m9z.DeviceStatus + Loops []DeviceStatusLoop +} + +// 实时监控数据 +func getDeviceStatus2(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + + read, err := m9z.DeviceStatusRead(cuid) + if err != nil { + panic(err) + } + d := new(DeviceStatus) + d.DeviceStatus = *read + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + var loop DeviceStatusLoop + loop.Name = fmt.Sprintf("#%+v", i+1) + loop.Index = i + loop.IsOpen = read.Relays[i] + loop.Pwm = int(read.PWMOutputs[i]) + parametersRead, err := m9z.SubLoopParametersRead(cuid, uint(i)) + if err != nil { + g.Log().Error(err) + return + } + if parametersRead != nil { + kw := float32(0.0) + loop.V = (parametersRead.DCVoltage * 10) / 10 + for _, v := range parametersRead.Modules { + loop.A = loop.A + v.OutputCurrent + kw = float32(math.Round(float64(v.OutputCurrent*v.OutputVoltage*1000))/1000) + kw + } + loop.Kw = ((kw / 1000) * 1000) / 1000 + loop.A = (loop.A * 100) / 100 + } + d.Loops = append(d.Loops, loop) + }() + + } + wg.Wait() + // 按 Timestamp 排序(最早的在前) + sort.Slice(d.Loops, func(i, j int) bool { + return d.Loops[i].Index < d.Loops[j].Index + }) + rpc.WriteResult(d) + +} + +// 获取设备的配置 +func getDeviceConfig(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + + read, err := m9z.DeviceConfigRead(cuid) + if err != nil { + panic(err) + } + rpc.WriteResult(read) +} + +// 获取日出日落时间 +func getDeviceSunRiseTime(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + + read, err := m9z.SunRiseTimeRead(cuid) + if err != nil { + panic(err) + } + rpc.WriteResult(read) + +} + +// 读取开始任务 (优化指令 CMD=0x13) +func getStartTask(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` // 0x00-0x02 + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + + read, err := m9z.StartTaskRead(p.CommUid, p.Mode) + if err != nil { + panic(err) + } + /*program, err := m9z.ControlTaskToTaskProgram(m9z.DefaultStopTask, p.Mode) + if err != nil { + panic(err) + }*/ + /*err = m9z.StartTaskWriteByTaskCfg(p.CommUid, p.Mode, m9z.DefaultStartTask) + if err != nil { + panic(err) + }*/ + + rpc.WriteResult(read) +} + +// 读取中间任务 (优化指令 CMD=0x14) +func getMiddleTaskRead(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` // 0x00-0x02 + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + + read, err := m9z.MiddleTaskRead(p.CommUid, p.Mode) + if err != nil { + panic(err) + } + rpc.WriteResult(read) +} + +// 读取结束任务 (优化指令 CMD=0x15) +func getStopTaskRead(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` // 0x00-0x02 + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + + read, err := m9z.StopTaskRead(p.CommUid, p.Mode) + if err != nil { + panic(err) + } + + rpc.WriteResult(read) +} +func getStartTask2(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` // 0x00-0x02 + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + + read, err := m9z.StartTaskRead(p.CommUid, p.Mode) + if err != nil { + panic(terror.Wrap(err, "")) + } + task, err := m9z.TaskProgramToControlTask(read) + if err != nil { + panic(err) + + } + if task.TimeType == 1 { + timeRead, err := m9z.SunRiseTimeRead(p.CommUid) + if err != nil { + panic(err) + } + task.ExecTime = timeRead.Sunrise + } + rpc.WriteResult(&m9z.TaskProgram{}) + +} +func getStopTask2(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` // 0x00-0x02 + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + + read, err := m9z.StopTaskRead(p.CommUid, p.Mode) + if err != nil { + panic(err) + } + task, err := m9z.TaskProgramToControlTask(read) + if err != nil { + panic(err) + } + if task.TimeType == 1 { + timeRead, err := m9z.SunRiseTimeRead(p.CommUid) + if err != nil { + panic(err) + } + task.ExecTime = timeRead.Sunset + } + + rpc.WriteResult(task) +} + +func getMiddleTask2(rpc towgo.JsonRpcConnection) { + + var p struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` // 0x00-0x02 + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + + read, err := m9z.MiddleTaskRead(p.CommUid, p.Mode) + if err != nil { + panic(err) + } + /*program, err := m9z.MiddleControlTaskToTaskProgram(m9z.DefaultMiddleTask, p.Mode) + if err != nil { + panic(err) + }*/ + task, err := m9z.TaskProgramToMiddleControlTask(read) + if err != nil { + panic(err) + } + + rpc.WriteResult(task) +} + +func getSubLoopParameters(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + Idx uint `json:"idx"` + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + + r, err := m9z.SubLoopParametersRead(p.CommUid, p.Idx) + if err != nil { + panic(err) + } + rpc.WriteResult(r) +} + +func getAllSubLoopParameter(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + + result := make(map[string]interface{}) + subLoopData := make([]*m9z.LoopFullParameters, 0) + subLoopMap := make(map[string]*m9z.LoopFullParameters) + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + subLoop, err := m9z.SubLoopParametersRead(p.CommUid, uint(i)) + if err != nil || subLoop == nil { + return + } + subLoopMap[subLoop.LoopAddress] = subLoop + }() + } + + wg.Wait() + for _, v := range subLoopMap { + subLoopData = append(subLoopData, v) + } + result["subLoopData"] = subLoopData + totalP := 0.0 + + for _, subLoop := range subLoopData { + totalP = totalP + float64(subLoop.DCVoltage*subLoop.DCCurrent) + } + result["totalP"] = totalP + rpc.WriteResult(result) +} diff --git a/internal/module/m9zTtyApi/set.go b/internal/module/m9zTtyApi/set.go new file mode 100644 index 0000000..0da0ca1 --- /dev/null +++ b/internal/module/m9zTtyApi/set.go @@ -0,0 +1,366 @@ +package m9zApi + +import ( + "fmt" + "strings" + g "tgk-touch/internal/global" + m9z "tgk-touch/internal/library/m9z" + "time" + + "github.com/towgo/towgo/errors/terror" + "github.com/towgo/towgo/towgo" +) + +// 写入设备编程模式 +func setDeviceMode(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` + } + rpc.ReadParams(&p) + var mode byte + mode = 0x00 + switch p.Mode { + case 0: + mode = 0x00 + break + case 1: + mode = 0x01 + break + case 2: + mode = 0x02 + break + } + err := m9z.ModeWrite(p.CommUid, mode) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + g.Log().Debugf("setDeviceMode: mode = %+v", p) + rpc.WriteResult("ok") +} + +func setDeviceManualInfo(rpc towgo.JsonRpcConnection) { + var p struct { + m9z.ManualData + CommUid string `json:"comm_uid"` + } + rpc.ReadParams(&p) + + err := m9z.ManualWrite(p.CommUid, &p.ManualData) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} + +// 设置设备时间 +func setDeviceTime(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + DeviceTime string `json:"device_time"` + } + rpc.ReadParams(&p) + deviceLgnLatReadRep, err := m9z.LgnLatRead(p.CommUid) + if err != nil { + panic(err) + } + + newTime, err := time.ParseInLocation("2006-01-02 15:04:05", p.DeviceTime, deviceLgnLatReadRep.Loc) + if err != nil { + panic(err) + } + err = m9z.DeviceTimeWrite(p.CommUid, newTime) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} + +func setLgnLatData(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + m9z.LgnLatData + LocStr string `json:"loc_str"` + } + rpc.ReadParams(&p) + ld := p.LgnLatData + loc, err := parseTimezoneOffset(p.LocStr) + if err != nil { + panic(err) + } + ld.Loc = loc + err = m9z.LgnLatWrite(p.CommUid, &ld) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} + +func parseTimezoneOffset(offsetStr string) (*time.Location, error) { + // 提取偏移量(如 "+08:00") + offsetStr = strings.TrimPrefix(offsetStr, "UTC") // 移除前缀 "UTC" + + // 解析小时和分钟 + sign := 1 + if offsetStr[0] == '-' { + sign = -1 + offsetStr = offsetStr[1:] + } else if offsetStr[0] == '+' { + offsetStr = offsetStr[1:] + } + + parts := strings.Split(offsetStr, ":") + hours, minutes := 0, 0 + if len(parts) >= 1 { + fmt.Sscanf(parts[0], "%d", &hours) + } + if len(parts) >= 2 { + fmt.Sscanf(parts[1], "%d", &minutes) + } + + // 计算总秒数 + totalSeconds := (hours*60 + minutes) * 60 * sign + return time.FixedZone(offsetStr, totalSeconds), nil +} + +// 设置日出日落时间偏差 +func setSunRiseSet(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + m9z.SunRiseSet + } + rpc.ReadParams(&p) + err := m9z.SunRiseSetWrite(p.CommUid, &p.SunRiseSet) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} + +// 设置设备的配置 +func setDeviceConfig(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + m9z.DeviceConfig + } + rpc.ReadParams(&p) + err := m9z.DeviceConfigWrite(p.CommUid, &p.DeviceConfig) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} + +func setSave(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + err := m9z.Save(cuid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} +func setRestart(rpc towgo.JsonRpcConnection) { + cuid, _ := getDeviceId(rpc) + err := m9z.Restart(cuid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} + +func setDeviceLgnLatAndSunRsTime(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + m9z.SunRiseSet + m9z.LgnLatData + LocStr string `json:"loc_str"` + } + rpc.ReadParams(&p) + ld := p.LgnLatData + if p.LocStr == "" { + p.LocStr = "UTC+08:00" + } + loc, err := parseTimezoneOffset(p.LocStr) + if err != nil { + panic(err) + } + ld.Loc = loc + err = m9z.LgnLatWrite(p.CommUid, &ld) + if err != nil { + panic(err) + } + err = m9z.SunRiseSetWrite(p.CommUid, &p.SunRiseSet) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") + +} + +// 写入开始任务 (优化指令 CMD=0x13) +func setStartTaskWrite(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + m9z.TaskProgram + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + err := m9z.StartTaskWrite(p.CommUid, &p.TaskProgram) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} + +// 写入中间任务 (优化指令 CMD=0x14) +func setMiddleTaskWrite(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + m9z.TaskProgram + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + err := m9z.MiddleTaskWrite(p.CommUid, &p.TaskProgram) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} + +// 写入结束任务 (优化指令 CMD=0x15) +func setStopTaskWrite(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + m9z.TaskProgram + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + err := m9z.StopTaskWrite(p.CommUid, &p.TaskProgram) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} + +// 写入结束任务 (优化指令 CMD=0x15) +func setStartTask2(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` + m9z.ControlTask + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + program, err := m9z.ControlTaskToTaskProgram(p.ControlTask, p.Mode) + if err != nil { + panic(err) + } + g.Log().Debugf("program: %+v", program) + err = m9z.StartTaskWrite(p.CommUid, program) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} +func setStopTask2(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` + m9z.ControlTask + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + program, err := m9z.ControlTaskToTaskProgram(p.ControlTask, p.Mode) + if err != nil { + panic(err) + } + + err = m9z.StopTaskWrite(p.CommUid, program) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} + +func setMiddleTask2(rpc towgo.JsonRpcConnection) { + var p struct { + CommUid string `json:"comm_uid"` + Mode uint8 `json:"mode"` + m9z.MiddleControlTask + } + rpc.ReadParams(&p) + if p.CommUid == "" { + panic(terror.New("comm_uid is empty")) + } + program, err := m9z.MiddleControlTaskToTaskProgram(p.MiddleControlTask, p.Mode) + if err != nil { + panic(err) + } + err = m9z.MiddleTaskWrite(p.CommUid, program) + if err != nil { + panic(err) + } + err = m9z.Save(p.CommUid) + if err != nil { + panic(err) + } + rpc.WriteResult("ok") +} diff --git a/internal/module/m9zTtyPwd/init.go b/internal/module/m9zTtyPwd/init.go new file mode 100644 index 0000000..d24ace2 --- /dev/null +++ b/internal/module/m9zTtyPwd/init.go @@ -0,0 +1,367 @@ +package m9zTtyPwd + +import ( + "crypto/md5" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + g "tgk-touch/internal/global" + "time" + + "github.com/towgo/towgo/lib/system" + "github.com/towgo/towgo/towgo" +) + +const ( + DefaultPassword = "123456" + DefaultSalt = "m9zTty" + SessionExpireMinutes = 10 +) + +var ( + manager *PasswordManager = &PasswordManager{ + verifiedSessions: make(map[string]time.Time), + } +) + +var noInterceptor bool + +var ( + whiteListMethods = map[string]bool{ + "/m9z/password/verify": true, + "/m9z/password/change": true, + // 实时监控 + "/m9z/touchdisplay/getDeviceID": true, + "/m9z/getDeviceStatus2": true, + "/getMessageInterval": true, + "/adl400/readPhaseData": true, + // 报警预览 + "/m9z/fault/list": true, + } + whiteListPrefixes = []string{ + //"/m9z/get", + //"/m9z/system/get", + //"/m9z/fault", + } +) + +type PasswordManager struct { + mu sync.RWMutex + hashedPwd string + salt string + verifiedSessions map[string]time.Time +} + +type PasswordData struct { + HashedPassword string `json:"hashed_password"` + Salt string `json:"salt"` +} + +func getPwdFilePath() string { + return filepath.Join(system.GetPathOfProgram(), "data", "m9zTtyPwd.json") +} + +func hashPassword(pwd, salt string) string { + data := []byte(pwd + salt) + sum := sha256.Sum256(data) + return hex.EncodeToString(sum[:]) +} + +func (m *PasswordManager) loadFromFile() error { + m.mu.Lock() + defer m.mu.Unlock() + filePath := getPwdFilePath() + data, err := os.ReadFile(filePath) + + if err != nil { + if os.IsNotExist(err) { + + m.hashedPwd = hashPassword(DefaultPassword, DefaultSalt) + m.salt = DefaultSalt + + return m.saveToFile() + } + return err + } + + var pwdData PasswordData + if err := json.Unmarshal(data, &pwdData); err != nil { + return err + } + + m.hashedPwd = pwdData.HashedPassword + m.salt = pwdData.Salt + return nil +} + +func (m *PasswordManager) saveToFile() error { + filePath := getPwdFilePath() + dir := filepath.Dir(filePath) + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + pwdData := PasswordData{ + HashedPassword: m.hashedPwd, + Salt: m.salt, + } + + data, err := json.Marshal(pwdData) + if err != nil { + return err + } + + return os.WriteFile(filePath, data, 0600) +} + +func (m *PasswordManager) verify(pwd string) bool { + m.mu.RLock() + defer m.mu.RUnlock() + return m.hashedPwd == hashPassword(pwd, m.salt) +} + +func (m *PasswordManager) change(newPwd string) error { + m.mu.Lock() + defer m.mu.Unlock() + m.hashedPwd = hashPassword(newPwd, m.salt) + return m.saveToFile() +} + +func (m *PasswordManager) addVerifiedSession(sessionId string) { + m.mu.Lock() + defer m.mu.Unlock() + m.verifiedSessions[sessionId] = time.Now().Add(SessionExpireMinutes * time.Minute) +} + +func (m *PasswordManager) isSessionVerified(sessionId string) bool { + m.mu.RLock() + defer m.mu.RUnlock() + expireTime, ok := m.verifiedSessions[sessionId] + if !ok { + return false + } + if time.Now().After(expireTime) { + return false + } + return true +} + +func (m *PasswordManager) removeSession(sessionId string) { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.verifiedSessions, sessionId) +} + +func (m *PasswordManager) cleanupExpiredSessions() { + m.mu.Lock() + defer m.mu.Unlock() + now := time.Now() + for sessionId, expireTime := range m.verifiedSessions { + if now.After(expireTime) { + delete(m.verifiedSessions, sessionId) + } + } +} + +func (m *PasswordManager) isWhiteListed(method string) bool { + if whiteListMethods[method] { + return true + } + for _, prefix := range whiteListPrefixes { + if strings.HasPrefix(method, prefix) { + return true + } + } + return false +} + +func NoInterceptor(b bool) { + noInterceptor = b +} + +func UnauthorizedMethodAdd(method string) { + whiteListMethods[method] = true +} + +func UnauthorizedMethodPrefixAdd(prefix string) { + whiteListPrefixes = append(whiteListPrefixes, prefix) +} + +var mu sync.Mutex + +func Init() { + mu.Lock() + defer mu.Unlock() + + if err := manager.loadFromFile(); err != nil { + g.GVA_LOG.Sugar().Warnf("load password data failed: %v, using default", err) + manager.hashedPwd = hashPassword(DefaultPassword, DefaultSalt) + manager.salt = DefaultSalt + manager.saveToFile() + } + + towgo.SetFunc("/m9z/password/verify", verifyPwd) + towgo.SetFunc("/m9z/password/change", changePwd) + towgo.SetFunc("/m9z/system/getTime", getSystemTime) + towgo.SetFunc("/m9z/system/setTime", setSystemTime) + + towgo.AddInterceptor(pwdInterceptor) + + go func() { + ticker := time.NewTicker(5 * time.Minute) + for range ticker.C { + manager.cleanupExpiredSessions() + } + }() +} + +func generateSessionId() string { + data := []byte(time.Now().String()) + sum := md5.Sum(data) + return hex.EncodeToString(sum[:]) +} + +func verifyPwd(rpcConn towgo.JsonRpcConnection) { + var req struct { + Password string `json:"password"` + } + rpcConn.ReadParams(&req) + + sessionId := generateSessionId() + if manager.verify(req.Password) { + manager.addVerifiedSession(sessionId) + rpcConn.WriteResult(map[string]interface{}{ + "success": true, + "session_id": sessionId, + "message": "verified", + }) + } else { + rpcConn.WriteError(401, "invalid password") + } +} + +type ChangePwdReq struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` +} + +func changePwd(rpcConn towgo.JsonRpcConnection) { + var req ChangePwdReq + rpcConn.ReadParams(&req) + + if !manager.verify(req.OldPassword) { + rpcConn.WriteError(500, "旧密码错误") + return + } + + if len(req.NewPassword) < 6 { + rpcConn.WriteError(500, "新密码长度小于 6") + return + } + + if err := manager.change(req.NewPassword); err != nil { + rpcConn.WriteError(500, "change password failed: "+err.Error()) + return + } + + rpcConn.WriteResult(map[string]interface{}{ + "success": true, + "message": "password changed", + }) +} + +func pwdInterceptor(conn towgo.JsonRpcConnection) error { + if noInterceptor { + return nil + } + + rpcRequest := conn.GetRpcRequest() + rpcResponse := conn.GetRpcResponse() + + if manager.isWhiteListed(rpcRequest.Method) { + return nil + } + + sessionId := rpcRequest.Session + if sessionId == "" { + rpcResponse.Error.Set(401, "password verification required") + conn.Write() + return errors.New("password verification required") + } + + if !manager.isSessionVerified(sessionId) { + rpcResponse.Error.Set(401, "session expired or invalid") + conn.Write() + return errors.New("password verification required") + } + + return nil +} + +func getSystemTime(rpcConn towgo.JsonRpcConnection) { + now := time.Now() + rpcConn.WriteResult(map[string]interface{}{ + "timestamp": now.Unix(), + "datetime": now.Format("2006-01-02 15:04:05"), + "timezone": now.Location().String(), + }) +} + +type SetTimeReq struct { + Datetime string `json:"datetime"` +} + +func setSystemTime(rpcConn towgo.JsonRpcConnection) { + var req SetTimeReq + rpcConn.ReadParams(&req) + + if req.Datetime == "" { + rpcConn.WriteError(400, "datetime is required") + return + } + + layouts := []string{ + "2006-01-02 15:04:05", + "2006-01-02T15:04:05Z", + "2006-01-02 15:04", + "2006-01-02", + } + + var targetTime time.Time + var parseErr error + for _, layout := range layouts { + targetTime, parseErr = time.Parse(layout, req.Datetime) + if parseErr == nil { + break + } + } + + if parseErr != nil { + rpcConn.WriteError(400, "invalid datetime format, supported: 2006-01-02 15:04:05, 2006-01-02T15:04:05Z, 2006-01-02 15:04, 2006-01-02") + return + } + + unixTime := targetTime.Unix() + cmd := exec.Command("date", "-s", targetTime.Format("2006-01-02 15:04:05")) + cmd.Run() + + cmd = exec.Command("hwclock", "-w") + cmd.Run() + + cmd = exec.Command("timedatectl", "set-ntp", "false") + cmd.Run() + + rpcConn.WriteResult(map[string]interface{}{ + "success": true, + "message": "system time updated", + "old_time": time.Now().Format("2006-01-02 15:04:05"), + "new_time": targetTime.Format("2006-01-02 15:04:05"), + "unix_time": unixTime, + }) +} diff --git a/internal/module/maincontrollerClient/mainControllerClient.go b/internal/module/maincontrollerClient/mainControllerClient.go new file mode 100644 index 0000000..e58fa03 --- /dev/null +++ b/internal/module/maincontrollerClient/mainControllerClient.go @@ -0,0 +1,246 @@ +package maincontrollerClient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "github.com/towgo/towgo/errors/terror" + "github.com/towgo/towgo/towgo" + "go.uber.org/zap" + "sync" + g "tgk-touch/internal/global" +) + +var controllerClient *MainControllerClient + +type Device struct { + ID int64 `json:"id" xorm:"pk"` + SerialNumber string `json:"serial_number" xorm:"index unique"` + Name string `json:"name"` + Classify string `json:"classify"` + IsOnline bool `json:"is_online" xorm:"-"` + LastSubsistTime int64 `json:"last_subsist_time"` + UpdatedAt int64 `xorm:"updated"` + CreatedAt int64 `xorm:"created"` +} + +func ControllerClient() *MainControllerClient { + if controllerClient == nil { + controllerClient = NewMainControllerClient(MainControllerClientConfig{ + ServerHost: g.Config().MainControllerClient.ServerHost, + SecretKey: g.Config().MainControllerClient.SecretKey, + }) + } + return controllerClient +} + +type MainControllerClientConfig struct { + ServerHost string `json:"server_host"` + SecretKey string `json:"secret_key"` +} + +type MainControllerClient struct { + MainControllerClientConfig + *towgo.WebScoketClient +} + +func NewMainControllerClient(config MainControllerClientConfig) *MainControllerClient { + g.Log().Info("初始化主控客户端") + ws := towgo.NewWebsocketClient(config.ServerHost, config.ServerHost) + m := &MainControllerClient{} + m.MainControllerClientConfig = config + m.WebScoketClient = ws + m.CallTimeOut = 10 + m.OnConnect = func(wsc *towgo.WebScoketClient) { + m.mainControllerClientReg(wsc) + } + m.OnClose = func(wsc *towgo.WebScoketClient) {} + m.Connect() + return m +} + +// 服务注册 +func (m *MainControllerClient) mainControllerClientReg(wsc *towgo.WebScoketClient) { + g.Log().Info("注册主控客户端到服务端") + request := towgo.NewJsonrpcrequest() + request.Method = "/iotserver/maincontroller/reg" + request.Params = m.MainControllerClientConfig + wsc.Call(request, func(jrc towgo.JsonRpcConnection) { + if jrc.GetRpcResponse().Error.Code != 200 { + g.Log().Errorf("mainControllerClientReg call err {%v}", jrc.GetRpcResponse().Error.Message) + } + }) + request.Await() +} +func getParamsType(params any) string { + switch params.(type) { + case []byte: + return "[]byte" + } + return "" +} + +// 定义日志格式化回调类型,由调用者自定义友好显示内容 +type LogFormatter func(deviceID, method string, params, result interface{}) (deviceType, cmdDetail, cmdDisplay, resultDisplay string) + +// 带日志记录的设备调用函数,新增formatter参数用于自定义日志格式 +func DeviceCallByDeviceID(deviceID, method, token string, params, destResult any, formatter LogFormatter) error { + var err error + // 最多重试3次 + for i := 0; i < 3; i++ { + err = deviceCallByDeviceID(deviceID, method, token, params, destResult) + if err == nil { + break + } + } + // 构建日志基础信息 + logEntry := New(deviceID, params, destResult) + + if err != nil { + logEntry.Error = err.Error() + } + + // 应用自定义格式化(如果提供) + if formatter != nil { + devType, cmdDetail, cmdDisp, resultDisp := formatter(deviceID, method, params, destResult) + if cmdDisp == "" { + cmdDisp = formatDefault(params) + } + if resultDisp == "" { + resultDisp = formatDefault(destResult) + } + logEntry.CmdDisply = cmdDisp + logEntry.ResultDisply = resultDisp + logEntry.DeviceType = devType + logEntry.CmdDetail = cmdDetail + } else { + // 默认格式化(处理字节切片等特殊类型) + logEntry.CmdDisply = formatDefault(params) + logEntry.ResultDisply = formatDefault(destResult) + } + zap.S().Debugf("%+v", logEntry) + return err +} + +// formatDefault 默认格式化:优先用%q处理字符串类,特殊类型单独适配 +func formatDefault(data interface{}) string { + if data == nil { + return "%q" // 明确标记nil,比"null"更符合%q的语义 + } + + // 1. 处理字符串:直接用%q,自动转义特殊字符 + if s, ok := data.(string); ok { + return fmt.Sprintf("%q", s) + } + + // 2. 处理字节切片:先转十六进制,再用%q包裹(避免乱码,且显式标记类型) + if b, ok := data.([]byte); ok { + // 若字节切片是可打印ASCII,优先转ASCII+%q;否则转十六进制 + if isPrintableASCII(b) { + return fmt.Sprintf("[]byte(ascii): %q", string(b)) + } + return fmt.Sprintf("[]byte(hex): %q", hex.EncodeToString(b)) + } + + // 3. 处理符文(rune):用%q显示字符+Unicode码点 + if r, ok := data.(rune); ok { + return fmt.Sprintf("rune: %q (U+%04X)", r, r) + } + + // 4. 复杂类型(结构体/切片/映射):用JSON序列化,字符串字段自动带%q效果 + // (JSON序列化会自动转义特殊字符,等价于%q的最小化转义) + jsonBytes, err := json.MarshalIndent(data, "", " ") + if err == nil { + if string(jsonBytes) != "null" && string(jsonBytes) != "" { + return fmt.Sprintf("json: %q", string(jsonBytes)) // 用%q包裹JSON字符串,显式转义双引号 + } + } + + // 5. 其他基础类型(int/bool/float等):用%+v(%q会多一层引号,冗余) + return fmt.Sprintf("%+v", data) +} + +// 辅助函数:判断字节切片是否全是可打印ASCII(32-126) +func isPrintableASCII(b []byte) bool { + for _, c := range b { + if c < 32 || c > 126 { + return false + } + } + return true +} + +var pushDeviceLock sync.Mutex + +func deviceCallByDeviceID(deviceID, method, token string, params, destResult any) error { + if serialPortMode { + pushDeviceLock.Lock() + defer pushDeviceLock.Unlock() + b, err := ToBytes(params) + if err != nil { + return err + } + result, err := serialPort.BIO(b) + if err != nil { + g.Log().Error("serialPort.BIO err:", err.Error()) + return terror.Wrap(err, "serialPort.BIO result err") + } + b, err = json.Marshal(result) + if err != nil { + g.Log().Error("serialPort.BIO json.Marshal(result) err:", err.Error()) + } + + return json.Unmarshal(b, destResult) + } + deviceRequest := towgo.NewJsonrpcrequest() + deviceRequest.Method = method + deviceRequest.Session = token + deviceRequest.Params = params + deviceRequest.DataType = getParamsType(params) + + mainControllerRequest := towgo.NewJsonrpcrequest() + + mainControllerRequest.Method = "/iotserver/maincontroller/device/call" + mainControllerRequest.Params = deviceRequest + mainControllerRequest.Route.DestAddr = deviceID + + var err error + ControllerClient(). + Call(mainControllerRequest, func(jrc towgo.JsonRpcConnection) { + //g.Log().Debug(jrc.GetRpcResponse()) + if jrc.GetRpcResponse().Error.Code != 200 { + err = terror.Newf("deviceId [%s],msg %s", deviceID, jrc.GetRpcResponse().Error.Message) + return + } + jrc.ReadResult(destResult) + }) + mainControllerRequest.Await() + + return err +} + +type DeviceControllerLog struct { + DeviceId string `json:"device_id" xorm:"comment('网关Id')" ` + DeviceType string `json:"device_type,omitempty" xorm:"comment('设备类型')"` + + CmdDetail string `json:"cmd_detail" xorm:"text comment('命令描述')"` + Cmd string `json:"cmd" xorm:"text notnull comment('指令%q字符串')" ` + + Result string `json:"result" xorm:"text notnull comment('结果%q字符串')" ` + + CmdDisply string `json:"cmd_disply" xorm:"text comment('命令友好显示')"` + ResultDisply string `json:"result_disply" xorm:"text comment('结果友好显示')"` + + Error string `json:"error" xorm:" varchar(255) comment('指令执行异常')" ` +} + +func (DeviceControllerLog) TableName() string { + return "device_controller_log" +} +func New(deviceId string, cmd, result interface{}) *DeviceControllerLog { + return &DeviceControllerLog{ + DeviceId: deviceId, + Cmd: formatDefault(cmd), + Result: formatDefault(result), + } +} diff --git a/internal/module/maincontrollerClient/serialport.go b/internal/module/maincontrollerClient/serialport.go new file mode 100644 index 0000000..6359075 --- /dev/null +++ b/internal/module/maincontrollerClient/serialport.go @@ -0,0 +1,246 @@ +package maincontrollerClient + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/json" + "github.com/towgo/towgo/errors/terror" + "io" + "reflect" + "sync" + "time" + + "log" + + "go.bug.st/serial" +) + +var serialPortMode bool +var serialPort *SerialPort + +// SerialPort 串口对象 +type SerialPort struct { + config *serial.Mode + address string + port serial.Port + isOpen bool + ctx context.Context + cancel context.CancelFunc + dataChan chan byte + onData []func([]byte) + ticket *time.Ticker + + isBIO bool + bioChan chan []byte + bioWriteLock sync.Mutex + bioLocker sync.Mutex +} + +// NewSerialPort 创建串口实例 +func NewSerialPort(portName string, baudRate int) *SerialPort { + return &SerialPort{ + address: portName, + + config: &serial.Mode{ + + BaudRate: baudRate, + }, + } +} + +func UseSerialPort(portName string, baudRate int) error { + serialPortMode = true + serialPort = &SerialPort{ + address: portName, + + config: &serial.Mode{ + BaudRate: baudRate, + }, + } + + err := serialPort.Open() + if err != nil { + return err + } + + serialPort.OnDataReceived(func(data []byte) { + t := time.NewTimer(time.Millisecond * 100) + if serialPort.IsBIO() { + <-t.C + for { + select { + case <-serialPort.bioChan: + continue + default: + serialPort.bioChan <- data + } + break + } + } else { + log.Printf("无效上传的数据(仅支持BIO模式),丢弃:% X", (data)) + } + }) + + return nil +} + +// Open 打开串口连接 +func (sp *SerialPort) Open() error { + + if sp.isOpen { + return terror.New("port is already open") + } + + port, err := serial.Open(sp.address, sp.config) + if err != nil { + return err + } + + sp.dataChan = make(chan byte, 1) + sp.bioChan = make(chan []byte, 1) + sp.port = port + sp.ctx, sp.cancel = context.WithCancel(context.Background()) + sp.isOpen = true + go sp.readLoop() + go sp.readChanLoop() + return nil +} + +func (sp *SerialPort) BIO(data []byte) ([]byte, error) { + sp.bioWriteLock.Lock() + defer sp.bioWriteLock.Unlock() + sp.SetisBIO(true) + + sp.port.Write(data) + timer := time.NewTimer(time.Second * 3) + select { + case <-timer.C: + sp.SetisBIO(false) + return nil, terror.New("数据读取超时") + case message := <-sp.bioChan: + sp.SetisBIO(false) + return message, nil + } +} + +// Close 关闭串口连接 +func (sp *SerialPort) Close() error { + + if !sp.isOpen { + return terror.New("port is already closed") + } + + // 执行关闭操作 + + err := sp.port.Close() + sp.isOpen = false + return err +} + +// OnDataReceived 注册数据接收事件回调 +func (sp *SerialPort) OnDataReceived(callback func([]byte)) { + sp.onData = append(sp.onData, callback) +} + +func (sp *SerialPort) readChanLoop() { + sp.ticket = time.NewTicker(time.Millisecond * 100) + + var buf []byte + for { + if !sp.isOpen { + return + } + select { + case <-sp.ctx.Done(): + return + case <-sp.ticket.C: + if len(buf) > 0 { + callback := sp.onData + for _, f := range callback { + go f(buf) + } + buf = []byte{} + } + case b := <-sp.dataChan: + buf = append(buf, b) + } + } +} + +// 读取循环 +func (sp *SerialPort) readLoop() { + buf := make([]byte, 1) + for { + select { + case <-sp.ctx.Done(): + return + default: + _, err := sp.port.Read(buf) + sp.ticket.Reset(time.Millisecond * 100) + if err != nil { + if err != io.EOF { + if sp.isOpen { + sp.port.Close() + sp.isOpen = false + } + } + return + } + + sp.dataChan <- buf[0] + } + } +} + +func ToBytes(data interface{}) ([]byte, error) { + if data == nil { + return nil, nil + } + + // 处理 []byte 和字符串 + switch v := data.(type) { + case []byte: + return v, nil + case string: + return []byte(v), nil + } + + // 处理基本类型 + val := reflect.ValueOf(data) + for { + if val.Kind() == reflect.Interface { + val = val.Elem() + continue + } + break + } + + switch val.Kind() { + case reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + buf := new(bytes.Buffer) + if err := binary.Write(buf, binary.BigEndian, data); err != nil { + return nil, err + } + return buf.Bytes(), nil + + default: + // 复杂类型使用JSON序列化 + return json.Marshal(data) + } + +} + +func (sp *SerialPort) SetisBIO(isBio bool) { + sp.bioLocker.Lock() + sp.isBIO = isBio + sp.bioLocker.Unlock() +} +func (sp *SerialPort) IsBIO() bool { + sp.bioLocker.Lock() + defer sp.bioLocker.Unlock() + return sp.isBIO +} diff --git a/utility/directory.go b/utility/directory.go new file mode 100644 index 0000000..461aba3 --- /dev/null +++ b/utility/directory.go @@ -0,0 +1,193 @@ +package utility + +import ( + "github.com/towgo/towgo/errors/terror" + "io" + "os" + "path/filepath" + "reflect" + "strings" + "tgk-touch/internal/global" + + "go.uber.org/zap" +) + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: PathExists +//@description: 文件目录是否存在 +//@param: path string +//@return: bool, error + +func PathExists(path string) (bool, error) { + fi, err := os.Stat(path) + if err == nil { + if fi.IsDir() { + return true, nil + } + return false, terror.New("存在同名文件") + } + if os.IsNotExist(err) { + return false, nil + } + return false, err +} + +//@author: [piexlmax](https://github.com/piexlmax) +//@function: CreateDir +//@description: 批量创建文件夹 +//@param: dirs ...string +//@return: err error + +func CreateDir(dirs ...string) (err error) { + for _, v := range dirs { + exist, err := PathExists(v) + if err != nil { + return err + } + if !exist { + g.GVA_LOG.Debug("create directory" + v) + if err := os.MkdirAll(v, os.ModePerm); err != nil { + g.GVA_LOG.Error("create directory"+v, zap.Any(" error:", err)) + return err + } + } + } + return err +} + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: FileMove +//@description: 文件移动供外部调用 +//@param: src string, dst string(src: 源位置,绝对路径or相对路径, dst: 目标位置,绝对路径or相对路径,必须为文件夹) +//@return: err error + +func FileMove(src string, dst string) (err error) { + if dst == "" { + return nil + } + src, err = filepath.Abs(src) + if err != nil { + return err + } + dst, err = filepath.Abs(dst) + if err != nil { + return err + } + revoke := false + dir := filepath.Dir(dst) +Redirect: + _, err = os.Stat(dir) + if err != nil { + err = os.MkdirAll(dir, 0o755) + if err != nil { + return err + } + if !revoke { + revoke = true + goto Redirect + } + } + return os.Rename(src, dst) +} + +func DeLFile(filePath string) error { + return os.RemoveAll(filePath) +} + +//@author: [rice] +//@function: FileCopy +//@description: 文件复制供外部调用 +//@param: src string, dst string(src: 源位置,绝对路径or相对路径, dst: 目标位置,绝对路径or相对路径,必须为文件夹) +//@return: err error + +func FileCopy(src string, dst string) (err error) { + if dst == "" { + return nil + } + src, err = filepath.Abs(src) + if err != nil { + return err + } + dst, err = filepath.Abs(dst) + if err != nil { + return err + } + + // 检查源文件是否存在 + srcInfo, err := os.Stat(src) + if err != nil { + return err + } + if srcInfo.IsDir() { + return terror.New("源路径是一个目录,不是文件") + } + + // 确保目标目录存在 + revoke := false + dir := filepath.Dir(dst) +Redirect: + _, err = os.Stat(dir) + if err != nil { + err = os.MkdirAll(dir, 0o755) + if err != nil { + return err + } + if !revoke { + revoke = true + goto Redirect + } + } + + // 打开源文件 + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + // 创建目标文件 + dstFile, err := os.Create(dst) + if err != nil { + return err + } + defer dstFile.Close() + + // 复制文件内容 + _, err = io.Copy(dstFile, srcFile) + if err != nil { + return err + } + + // 保持文件权限一致 + return os.Chmod(dst, srcInfo.Mode()) +} + +//@author: [songzhibin97](https://github.com/songzhibin97) +//@function: TrimSpace +//@description: 去除结构体空格 +//@param: target interface (target: 目标结构体,传入必须是指针类型) +//@return: null + +func TrimSpace(target interface{}) { + t := reflect.TypeOf(target) + if t.Kind() != reflect.Ptr { + return + } + t = t.Elem() + v := reflect.ValueOf(target).Elem() + for i := 0; i < t.NumField(); i++ { + switch v.Field(i).Kind() { + case reflect.String: + v.Field(i).SetString(strings.TrimSpace(v.Field(i).String())) + } + } +} + +// FileExist 判断文件是否存在 +func FileExist(path string) bool { + fi, err := os.Lstat(path) + if err == nil { + return !fi.IsDir() + } + return !os.IsNotExist(err) +} diff --git a/utility/paramsCheck.go b/utility/paramsCheck.go new file mode 100644 index 0000000..e4d75fd --- /dev/null +++ b/utility/paramsCheck.go @@ -0,0 +1,107 @@ +package utility + +import ( + "fmt" + "github.com/gogf/gf/v2/util/gconv" + "reflect" + "strings" +) + +/* +* +数字转成固定长度字符串,并在空缺位置补足固定字符串 +num 数字 +str 空缺替代字符串 +l 长度 +*/ +func SuppLenZero(num int64, str string, l int) string { + lenStr := gconv.String(num) + if len(lenStr) == l { + return lenStr + } + if len(lenStr) < l { + count := l - len(lenStr) + for i := 0; i < count; i++ { + lenStr = str + lenStr + } + } else { + temp := "" + for i := 0; i < l; i++ { + temp += str + } + return temp + } + return lenStr +} +func CheckNum(num int64) int64 { + if num > 100 { + return 100 + } + if num < 0 { + return 0 + } + return num + +} +func FormatStringSliceToSQL(s []string) string { + var builder strings.Builder + builder.WriteString("(") + for i, v := range s { + if i > 0 { + builder.WriteString(", ") + } + builder.WriteString("'") + builder.WriteString(v) + builder.WriteString("'") + } + builder.WriteString(")") + return builder.String() +} + +// FormatSliceToSQL 接受一个任意类型的切片,并将其格式化为类似 ('a', 'b', 'c') 的字符串 +func FormatSliceToSQL(slice interface{}) (string, error) { + // 使用反射获取切片的类型信息 + v := reflect.ValueOf(slice) + if v.Kind() != reflect.Slice { + return "", fmt.Errorf("input is not a slice") + } + + var builder strings.Builder + + length := v.Len() + for i := 0; i < length; i++ { + if i > 0 { + builder.WriteString(", ") + } + element := v.Index(i).Interface() + // 根据元素类型进行格式化 + switch element.(type) { + case string: + builder.WriteString("'") + builder.WriteString(element.(string)) + builder.WriteString("'") + case int, int8, int16, int32, int64: + builder.WriteString("'") + + builder.WriteString(gconv.String(reflect.ValueOf(element).Int())) + builder.WriteString("'") + + case uint, uint8, uint16, uint32, uint64: + builder.WriteString("'") + + builder.WriteString(gconv.String(reflect.ValueOf(element).Uint())) + builder.WriteString("'") + + case float32, float64: + builder.WriteString("'") + + builder.WriteString(gconv.String(reflect.ValueOf(element).Float())) + builder.WriteString("'") + + default: + return "", fmt.Errorf("unsupported type: %T", element) + } + } + + return builder.String(), nil +} diff --git a/utility/timer/timed_task.go b/utility/timer/timed_task.go new file mode 100644 index 0000000..8916cb2 --- /dev/null +++ b/utility/timer/timed_task.go @@ -0,0 +1,237 @@ +package timer + +import ( + "github.com/robfig/cron/v3" + "sync" +) + +type Timer interface { + // 寻找所有Cron + FindCronList() map[string]*taskManager + // 添加Task 方法形式以秒的形式加入 + AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) // 添加Task Func以秒的形式加入 + // 添加Task 接口形式以秒的形式加入 + AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) + // 通过函数的方法添加任务 + AddTaskByFunc(cronName string, spec string, task func(), taskName string, option ...cron.Option) (cron.EntryID, error) + // 通过接口的方法添加任务 要实现一个带有 Run方法的接口触发 + AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) + // 获取对应taskName的cron 可能会为空 + FindCron(cronName string) (*taskManager, bool) + // 指定cron开始执行 + StartCron(cronName string) + // 指定cron停止执行 + StopCron(cronName string) + // 查找指定cron下的指定task + FindTask(cronName string, taskName string) (*task, bool) + // 根据id删除指定cron下的指定task + RemoveTask(cronName string, id int) + // 根据taskName删除指定cron下的指定task + RemoveTaskByName(cronName string, taskName string) + // 清理掉指定cronName + Clear(cronName string) + // 停止所有的cron + Stop() + Start() +} + +type task struct { + EntryID cron.EntryID + Spec string + TaskName string +} + +type taskManager struct { + corn *cron.Cron + tasks map[cron.EntryID]*task +} + +// timer 定时任务管理 +type timer struct { + cronList map[string]*taskManager + sync.Mutex +} + +// AddTaskByFunc 通过函数的方法添加任务 +func (t *timer) AddTaskByFunc(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddFunc(spec, fun) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByFuncWithSecond 通过函数的方法使用WithSeconds添加任务 +func (t *timer) AddTaskByFuncWithSecond(cronName string, spec string, fun func(), taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + option = append(option, cron.WithSeconds()) + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddFunc(spec, fun) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByJob 通过接口的方法添加任务 +func (t *timer) AddTaskByJob(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddJob(spec, job) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// AddTaskByJobWithSeconds 通过接口的方法添加任务 +func (t *timer) AddTaskByJobWithSeconds(cronName string, spec string, job interface{ Run() }, taskName string, option ...cron.Option) (cron.EntryID, error) { + t.Lock() + defer t.Unlock() + option = append(option, cron.WithSeconds()) + if _, ok := t.cronList[cronName]; !ok { + tasks := make(map[cron.EntryID]*task) + t.cronList[cronName] = &taskManager{ + corn: cron.New(option...), + tasks: tasks, + } + } + id, err := t.cronList[cronName].corn.AddJob(spec, job) + t.cronList[cronName].corn.Start() + t.cronList[cronName].tasks[id] = &task{ + EntryID: id, + Spec: spec, + TaskName: taskName, + } + return id, err +} + +// FindCron 获取对应cronName的cron 可能会为空 +func (t *timer) FindCron(cronName string) (*taskManager, bool) { + t.Lock() + defer t.Unlock() + v, ok := t.cronList[cronName] + return v, ok +} + +// FindTask 获取对应cronName的cron 可能会为空 +func (t *timer) FindTask(cronName string, taskName string) (*task, bool) { + t.Lock() + defer t.Unlock() + v, ok := t.cronList[cronName] + if !ok { + return nil, ok + } + for _, t2 := range v.tasks { + if t2.TaskName == taskName { + return t2, true + } + } + return nil, false +} + +// FindCronList 获取所有的任务列表 +func (t *timer) FindCronList() map[string]*taskManager { + t.Lock() + defer t.Unlock() + return t.cronList +} + +// StartCron 开始任务 +func (t *timer) StartCron(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Start() + } +} + +// StopCron 停止任务 +func (t *timer) StopCron(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Stop() + } +} + +// RemoveTask 从cronName 删除指定任务 +func (t *timer) RemoveTask(cronName string, id int) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Remove(cron.EntryID(id)) + delete(v.tasks, cron.EntryID(id)) + } +} + +// RemoveTaskByName 从cronName 使用taskName 删除指定任务 +func (t *timer) RemoveTaskByName(cronName string, taskName string) { + fTask, ok := t.FindTask(cronName, taskName) + if !ok { + return + } + t.RemoveTask(cronName, int(fTask.EntryID)) +} + +// Clear 清除任务 +func (t *timer) Clear(cronName string) { + t.Lock() + defer t.Unlock() + if v, ok := t.cronList[cronName]; ok { + v.corn.Stop() + delete(t.cronList, cronName) + } +} +func (t *timer) Start() { + t.Lock() + defer t.Unlock() + for _, v := range t.cronList { + v.corn.Start() + } +} + +// Close 释放资源 +func (t *timer) Stop() { + t.Lock() + defer t.Unlock() + for _, v := range t.cronList { + v.corn.Stop() + } +} + +func NewTimerTask() Timer { + return &timer{cronList: make(map[string]*taskManager)} +}