Rice 7 months ago
commit cefe5ee26c

1
.gitignore vendored

@ -0,0 +1 @@
/.idea/

@ -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
)

499
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=

@ -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
}

@ -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
})
}

@ -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()
}

@ -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服务已关闭")
}

@ -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
}

@ -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
}

@ -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,
}
}

@ -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)
}

@ -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)
}

@ -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: "正向有功谷电能"}
// ===== 反向有功<E69C89><E58A9F>能 =====
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
}

@ -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("响应<E5938D><E5BA94><EFBFBD>度不足: %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 解析写入多个保持<E4BF9D><E68C81><EFBFBD>存器响应
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])
}
}

@ -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
}

@ -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
}

@ -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])
}
}

@ -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
}

@ -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)
}

@ -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
}

@ -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
}

@ -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
}

@ -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
}

@ -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
}

@ -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
}

@ -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
}

@ -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
}

@ -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
}

@ -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)
}

@ -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
}

@ -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
}

@ -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 := &params.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 := &params.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)
}

@ -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
}

@ -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
}

@ -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
}

@ -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<<i) != 0) {
task.Loops[i].Pwm = pwmVal
}
}
}
}
}
// 如果没有找到时间指令,返回错误
if !foundTimeInstruction {
return nil, fmt.Errorf("missing time instruction in task program %+v", program.Instructions)
}
/* // 过滤掉保持默认关闭状态的回路
activeLoops := make([]TaskLoop, 0, len(task.Loops))
for _, loop := range task.Loops {
if loop.TurnOn || loop.Pwm > 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)
}

@ -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
}

@ -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)
}

@ -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
}

@ -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
}

@ -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)})
}

@ -0,0 +1,4 @@
package Adl400Impl
type Adl400Impl struct {
}

@ -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
}

@ -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
}

@ -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)
}

@ -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
}

@ -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
}

@ -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"`
}
)

@ -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)
}
}

@ -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"))
}

@ -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, &registers)
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
}

@ -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")
}

@ -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('统计能耗')"`
}

@ -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"`
}
)

@ -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"`
}

@ -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())
}

@ -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(&params)
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 ""
}

@ -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)
}

@ -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已处理')"`
}

@ -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")
}

@ -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)
}

@ -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")
}

@ -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,
})
}

@ -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),
}
}

@ -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
}

@ -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)
}

@ -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
}

@ -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)}
}
Loading…
Cancel
Save