├── .github └── workflows │ ├── go.yml │ └── releaser.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── alarm ├── alarm.go └── alarm_api.go ├── assets ├── alarm.png ├── device.png ├── map.png ├── product.png ├── project.png ├── protocol.png └── space.png ├── bin ├── bytes.go ├── checksum.go └── hex.go ├── build.sh ├── calc └── lang.go ├── cmd └── main.go ├── device ├── device.go └── device_api.go ├── go.mod ├── go.sum ├── internal ├── boot.go ├── device.go ├── device_api.go ├── mqtt.go └── validator.go ├── main.go ├── manifest.json ├── pages ├── alarm.json ├── device-choose.json ├── device-create.json ├── device-detail.json ├── device-edit.json ├── device-history.json ├── device-import.json ├── device-model.json ├── device-values-setting.json ├── device-values.json ├── device.json ├── license.json ├── product-choose.json ├── product-config.json ├── product-create.json ├── product-detail.json ├── product-device.json ├── product-edit.json ├── product-model.json ├── product.json ├── project-choose.json ├── project-create.json ├── project-detail.json ├── project-device-choose.json ├── project-device.json ├── project-edit.json ├── project-plugin.json ├── project-user.json ├── project.json ├── protocol.json ├── space-choose.json ├── space-create.json ├── space-detail.json ├── space-device.json ├── space-edit.json └── space.json ├── product ├── config.go ├── model.go ├── point.go ├── product.go ├── product_api.go └── validator.go ├── project ├── app-api.go ├── device-api.go ├── project-api.go ├── project.go └── user-api.go ├── protocol ├── api.go ├── protocol.go └── store.go ├── protocols └── modbus.json ├── space ├── device-api.go ├── space-api.go └── space.go └── test └── main.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Go 21 | uses: actions/setup-go@v4 22 | with: 23 | go-version: '1.23' 24 | 25 | - name: Build 26 | run: go build -v cmd/main.go 27 | 28 | - name: Test 29 | run: go test -v ./... 30 | -------------------------------------------------------------------------------- /.github/workflows/releaser.yml: -------------------------------------------------------------------------------- 1 | name: Releaser 2 | 3 | permissions: 4 | contents: write 5 | id-token: write 6 | packages: write 7 | 8 | on: 9 | push: 10 | tags: 11 | - 'v*.*.*' #当推送的标签符合 vX.Y.Z 格式时触发 12 | 13 | jobs: 14 | goreleaser: 15 | runs-on: ubuntu-latest 16 | env: 17 | flags: '' 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Go 23 | uses: actions/setup-go@v4 24 | with: 25 | go-version: 1.23 26 | cache: true 27 | 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v6 30 | with: 31 | distribution: goreleaser 32 | version: latest 33 | args: release --clean ${{ env.flags }} 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | /.idea 27 | /tstorage 28 | /apps 29 | /licenses 30 | /plugins 31 | /iot-master 32 | /iot-master.yaml 33 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: iot-master 3 | before: 4 | hooks: 5 | - go mod tidy 6 | builds: 7 | - env: 8 | - CGO_ENABLED=0 9 | id: "iot-master" 10 | main: ./cmd 11 | binary: "iot-master" 12 | ldflags: 13 | - -s -w -X main.build={{.Version}} 14 | goos: 15 | - windows 16 | - linux 17 | #- darwin 18 | goarch: 19 | - "386" 20 | - amd64 21 | - arm 22 | - arm64 23 | #- mips 24 | - mipsle #默认小端 25 | #- mips64 26 | - mips64le 27 | - loong64 #龙芯 28 | #- riscv32 29 | - riscv64 30 | goarm: 31 | - 6 32 | - 7 33 | gomips: 34 | - hardfloat 35 | - softfloat 36 | ignore: 37 | - goos: linux 38 | goarch: "386" 39 | - goos: windows 40 | goarch: arm 41 | - goos: windows 42 | goarch: arm64 43 | - goarch: mips64le 44 | gomips: softfloat 45 | 46 | archives: 47 | - wrap_in_directory: true 48 | #name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 49 | format_overrides: 50 | - goos: windows 51 | format: zip 52 | files: 53 | - README.md 54 | - LICENSE 55 | checksum: 56 | name_template: 'checksums.txt' 57 | changelog: 58 | use: github-native 59 | sort: asc 60 | filters: 61 | exclude: 62 | - '^docs:' 63 | - '^test:' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 物联大师 2 | 3 | [![Go](https://github.com/god-jason/iot-master/actions/workflows/go.yml/badge.svg)](https://github.com/god-jason/iot-master/actions/workflows/go.yml) 4 | 5 | 物联大师是开源免费的物联网数据中台,区别于传统的物联网平台,物联大师采用Go语言进行编程实现,有以下诸多优点: 6 | - 单一程序文件,免安装 7 | - 最低内存只需要12MB 8 | - 插件机制,支持功能自由扩展 9 | - 能够支持多种操作系统和处理器架构 10 | - 支持使用Lua脚本扩展协议 11 | - 支持使用JS脚本实现边缘计算 12 | - 支持智能家居应用场景,定时和联动控制 13 | - 支持WebRTC点对点视频传输 14 | 15 | 物联大师,物联小白 是本易物联网的一部分,更多信息请关注:[链接](https://busycloud.cn) 16 | 17 | 18 | ## 南向协议库 19 | - [x] Modbus RTU 20 | - [x] Modbus TCP 21 | - [ ] Modbus ASCII(使用比较少,暂不做支持) 22 | - [ ] PLC 23 | - - [x] Siemens 西门子 s7 fetchwrite mpi ppi 24 | - - [x] Mitsubishi 三菱 melsec 25 | - - [x] Omron 欧姆龙 fins hostlink 26 | - - [ ] AB df1 27 | - - [ ] Delta 台达 dvp 28 | - - [ ] Keyence 基恩士 melsec 29 | - - [ ] Panasonic 松下 melsec newtocol 30 | - - [ ] Fuji 富士 spb 31 | - - [ ] Fatek 永宏 32 | - [ ] BACnet智能建筑协议 33 | - [ ] KNX智能建筑协议 34 | - [x] DL/T645-1997、2007 多功能电表通讯规约 35 | - [ ] DL/T698.45-2017 国网电力通讯规约 36 | - [x] CJ/T188-2004、2018 户用计量仪表数据传输技术条件 37 | - [ ] IEC 101/103/104 电力系统远程控制和监视的通信协议 38 | - [ ] IEC 61850 电力系统自动化领域全球通用协议 39 | - [ ] SL/T427-2021 水资源监测数据传输规约 40 | - [ ] SL/T651-2014 水文监测数据通信规约 41 | - [ ] SL/T812.1-2021 水利监测数据传输规约 42 | - [ ] SZY206-2016 水资源监测数据传输规约 43 | 44 | 45 | ## 北向云平台 46 | - [x] 本易物联设备托管云 47 | - [x] HTTP 48 | - [x] MQTT 49 | - [ ] 主流云平台 50 | - - [ ] 阿里云 51 | - - [ ] 腾讯云 52 | - - [ ] 华为云 53 | - - [ ] 百度云 54 | - - [ ] 京东云 55 | - - [ ] 涂鸦云 56 | - - [ ] OneNET 57 | 58 | 59 | 60 | ## 联系方式 61 | 62 | 南京本易物联网有限公司 63 | 64 | - 邮箱:[jason@zgwit.com](mailto:jason@zgwit.com) 65 | - 手机:[15161515197](tel:15161515197)(微信同号) 66 | 67 | ## 开源协议 68 | 69 | [GNU GPLv3](https://github.com/god-jason/iot-master/blob/main/LICENSE) 70 | 71 | `补充:产品仅限个人免费使用,商业需求请联系我们` 72 | -------------------------------------------------------------------------------- /alarm/alarm.go: -------------------------------------------------------------------------------- 1 | package alarm 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/db" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | db.Register(&Alarm{}) 10 | } 11 | 12 | type Alarm struct { 13 | Id int64 `json:"id,omitempty"` 14 | DeviceId string `json:"device_id,omitempty" xorm:"index"` 15 | ProjectId string `json:"project_id,omitempty" xorm:"index"` 16 | Device string `json:"device,omitempty" xorm:"-"` 17 | Project string `json:"project,omitempty" xorm:"-"` 18 | Title string `json:"title,omitempty"` 19 | Message string `json:"message,omitempty"` 20 | Level int `json:"level,omitempty"` 21 | Created time.Time `json:"created,omitempty" xorm:"created"` 22 | } 23 | -------------------------------------------------------------------------------- /alarm/alarm_api.go: -------------------------------------------------------------------------------- 1 | package alarm 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/api" 5 | "github.com/busy-cloud/boat/curd" 6 | ) 7 | 8 | func init() { 9 | api.Register("GET", "iot/alarm/list", curd.ApiList[Alarm]()) 10 | api.Register("POST", "iot/alarm/search", curd.ApiSearch[Alarm]()) 11 | api.Register("GET", "iot/alarm/:id/delete", curd.ApiDelete[Alarm]()) 12 | } 13 | -------------------------------------------------------------------------------- /assets/alarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/god-jason/iot-master/c3fad692c9809a6dc32ac4b416244e47691c630d/assets/alarm.png -------------------------------------------------------------------------------- /assets/device.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/god-jason/iot-master/c3fad692c9809a6dc32ac4b416244e47691c630d/assets/device.png -------------------------------------------------------------------------------- /assets/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/god-jason/iot-master/c3fad692c9809a6dc32ac4b416244e47691c630d/assets/map.png -------------------------------------------------------------------------------- /assets/product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/god-jason/iot-master/c3fad692c9809a6dc32ac4b416244e47691c630d/assets/product.png -------------------------------------------------------------------------------- /assets/project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/god-jason/iot-master/c3fad692c9809a6dc32ac4b416244e47691c630d/assets/project.png -------------------------------------------------------------------------------- /assets/protocol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/god-jason/iot-master/c3fad692c9809a6dc32ac4b416244e47691c630d/assets/protocol.png -------------------------------------------------------------------------------- /assets/space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/god-jason/iot-master/c3fad692c9809a6dc32ac4b416244e47691c630d/assets/space.png -------------------------------------------------------------------------------- /bin/bytes.go: -------------------------------------------------------------------------------- 1 | package bin 2 | 3 | import "math" 4 | 5 | //ParseUint64 解析 6 | func ParseUint64(b []byte) uint64 { 7 | return (uint64(b[0]) << 56) | 8 | (uint64(b[1]) << 48) | 9 | (uint64(b[2]) << 40) | 10 | (uint64(b[3]) << 32) | 11 | (uint64(b[4]) << 24) | 12 | (uint64(b[5]) << 16) | 13 | (uint64(b[6]) << 8) | 14 | uint64(b[7]) 15 | } 16 | 17 | //ParseUint64LittleEndian 解析 18 | func ParseUint64LittleEndian(b []byte) uint64 { 19 | return (uint64(b[7]) << 56) | 20 | (uint64(b[6]) << 48) | 21 | (uint64(b[5]) << 40) | 22 | (uint64(b[4]) << 32) | 23 | (uint64(b[3]) << 24) | 24 | (uint64(b[2]) << 16) | 25 | (uint64(b[1]) << 8) | 26 | uint64(b[0]) 27 | } 28 | 29 | //ParseUint32 解析 30 | func ParseUint32(buf []byte) uint32 { 31 | return uint32(buf[0])<<24 + 32 | uint32(buf[1])<<16 + 33 | uint32(buf[2])<<8 + 34 | uint32(buf[3]) 35 | } 36 | 37 | //ParseUint32LittleEndian 解析 38 | func ParseUint32LittleEndian(buf []byte) uint32 { 39 | return uint32(buf[3])<<24 + 40 | uint32(buf[2])<<16 + 41 | uint32(buf[1])<<8 + 42 | uint32(buf[0]) 43 | } 44 | 45 | //ParseUint16 解析 46 | func ParseUint16(buf []byte) uint16 { 47 | return uint16(buf[0])<<8 + uint16(buf[1]) 48 | } 49 | 50 | //ParseUint16LittleEndian 解析 51 | func ParseUint16LittleEndian(buf []byte) uint16 { 52 | return uint16(buf[1])<<8 + uint16(buf[0]) 53 | } 54 | 55 | //ParseFloat32 解析 56 | func ParseFloat32(buf []byte) float32 { 57 | val := ParseUint32(buf) 58 | return math.Float32frombits(val) 59 | } 60 | 61 | //ParseFloat32LittleEndian 解析 62 | func ParseFloat32LittleEndian(buf []byte) float32 { 63 | val := ParseUint32LittleEndian(buf) 64 | return math.Float32frombits(val) 65 | } 66 | 67 | //ParseFloat64 解析 68 | func ParseFloat64(buf []byte) float64 { 69 | val := ParseUint64(buf) 70 | return math.Float64frombits(val) 71 | } 72 | 73 | //ParseFloat64LittleEndian 解析 74 | func ParseFloat64LittleEndian(buf []byte) float64 { 75 | val := ParseUint64LittleEndian(buf) 76 | return math.Float64frombits(val) 77 | } 78 | 79 | //Uint32ToBytes 编码 80 | func Uint32ToBytes(value uint32) []byte { 81 | buf := make([]byte, 4) 82 | buf[0] = byte(value >> 24) 83 | buf[1] = byte(value >> 16) 84 | buf[2] = byte(value >> 8) 85 | buf[3] = byte(value) 86 | return buf 87 | } 88 | 89 | //Uint32ToBytesLittleEndian 编码 90 | func Uint32ToBytesLittleEndian(value uint32) []byte { 91 | buf := make([]byte, 4) 92 | buf[3] = byte(value >> 24) 93 | buf[2] = byte(value >> 16) 94 | buf[1] = byte(value >> 8) 95 | buf[0] = byte(value) 96 | return buf 97 | } 98 | 99 | //Uint16ToBytes 编码 100 | func Uint16ToBytes(value uint16) []byte { 101 | buf := make([]byte, 2) 102 | buf[0] = byte(value >> 8) 103 | buf[1] = byte(value) 104 | return buf 105 | } 106 | 107 | //Uint16ToBytesLittleEndian 编码 108 | func Uint16ToBytesLittleEndian(value uint16) []byte { 109 | buf := make([]byte, 2) 110 | buf[1] = byte(value >> 8) 111 | buf[0] = byte(value) 112 | return buf 113 | } 114 | 115 | //WriteUint64 编码 116 | func WriteUint64(buf []byte, value uint64) { 117 | buf[0] = byte(value >> 56) 118 | buf[1] = byte(value >> 48) 119 | buf[2] = byte(value >> 40) 120 | buf[3] = byte(value >> 32) 121 | buf[4] = byte(value >> 24) 122 | buf[5] = byte(value >> 16) 123 | buf[6] = byte(value >> 8) 124 | buf[7] = byte(value) 125 | } 126 | 127 | //WriteUint64LittleEndian 编码 128 | func WriteUint64LittleEndian(buf []byte, value uint64) { 129 | buf[7] = byte(value >> 56) 130 | buf[6] = byte(value >> 48) 131 | buf[5] = byte(value >> 40) 132 | buf[4] = byte(value >> 32) 133 | buf[3] = byte(value >> 24) 134 | buf[2] = byte(value >> 16) 135 | buf[1] = byte(value >> 8) 136 | buf[0] = byte(value) 137 | } 138 | 139 | //WriteUint32 编码 140 | func WriteUint32(buf []byte, value uint32) { 141 | buf[0] = byte(value >> 24) 142 | buf[1] = byte(value >> 16) 143 | buf[2] = byte(value >> 8) 144 | buf[3] = byte(value) 145 | } 146 | 147 | //WriteUint32LittleEndian 编码 148 | func WriteUint32LittleEndian(buf []byte, value uint32) { 149 | buf[3] = byte(value >> 24) 150 | buf[2] = byte(value >> 16) 151 | buf[1] = byte(value >> 8) 152 | buf[0] = byte(value) 153 | } 154 | 155 | //WriteUint24 编码 156 | func WriteUint24(buf []byte, value uint32) { 157 | buf[0] = byte(value >> 16) 158 | buf[1] = byte(value >> 8) 159 | buf[2] = byte(value) 160 | } 161 | 162 | //WriteUint24LittleEndian 编码 163 | func WriteUint24LittleEndian(buf []byte, value uint32) { 164 | buf[2] = byte(value >> 16) 165 | buf[1] = byte(value >> 8) 166 | buf[0] = byte(value) 167 | } 168 | 169 | //WriteUint16 编码 170 | func WriteUint16(buf []byte, value uint16) { 171 | buf[0] = byte(value >> 8) 172 | buf[1] = byte(value) 173 | } 174 | 175 | //WriteUint16LittleEndian 编码 176 | func WriteUint16LittleEndian(buf []byte, value uint16) { 177 | buf[1] = byte(value >> 8) 178 | buf[0] = byte(value) 179 | } 180 | 181 | //WriteFloat32 编码 182 | func WriteFloat32(buf []byte, value float32) { 183 | val := math.Float32bits(value) 184 | WriteUint32(buf, val) 185 | } 186 | 187 | //WriteFloat32LittleEndian 编码 188 | func WriteFloat32LittleEndian(buf []byte, value float32) { 189 | val := math.Float32bits(value) 190 | WriteUint32LittleEndian(buf, val) 191 | } 192 | 193 | //WriteFloat64 编码 194 | func WriteFloat64(buf []byte, value float64) { 195 | val := math.Float64bits(value) 196 | WriteUint64(buf, val) 197 | } 198 | 199 | //WriteFloat64LittleEndian 编码 200 | func WriteFloat64LittleEndian(buf []byte, value float64) { 201 | val := math.Float64bits(value) 202 | WriteUint64LittleEndian(buf, val) 203 | } 204 | 205 | //BoolToAscii 编码 206 | func BoolToAscii(buf []byte) []byte { 207 | length := len(buf) 208 | ret := make([]byte, length) 209 | for i := 0; i < length; i++ { 210 | if buf[i] == 0 { 211 | ret[i] = '0' 212 | } else { 213 | ret[i] = '1' 214 | } 215 | } 216 | return ret 217 | } 218 | 219 | //AsciiToBool 编码 220 | func AsciiToBool(buf []byte) []byte { 221 | length := len(buf) 222 | ret := make([]byte, length) 223 | for i := 0; i < length; i++ { 224 | if buf[i] == '0' { 225 | ret[i] = 0 226 | } else { 227 | ret[i] = 1 228 | } 229 | } 230 | return ret 231 | } 232 | 233 | //Dup 复制 234 | func Dup(buf []byte) []byte { 235 | b := make([]byte, len(buf)) 236 | copy(b, buf) 237 | return b 238 | } 239 | 240 | //BoolToByte 编码 241 | func BoolToByte(buf []bool) []byte { 242 | r := make([]byte, len(buf)) 243 | for i, v := range buf { 244 | if v { 245 | r[i] = 1 246 | } 247 | } 248 | return r 249 | } 250 | 251 | //ByteToBool 编码 252 | func ByteToBool(buf []byte) []bool { 253 | r := make([]bool, len(buf)) 254 | for i, v := range buf { 255 | if v > 0 { 256 | r[i] = true 257 | } 258 | } 259 | return r 260 | } 261 | 262 | //ShrinkBool 压缩布尔类型 263 | func ShrinkBool(buf []byte) []byte { 264 | length := len(buf) 265 | //length = length % 8 == 0 ? length / 8 : length / 8 + 1; 266 | ln := length >> 3 // length/8 267 | if length&0x07 > 0 { // length%8 268 | ln++ 269 | } 270 | 271 | b := make([]byte, ln) 272 | 273 | for i := 0; i < length; i++ { 274 | if buf[i] > 0 { 275 | //b[i/8] += 1 << (i % 8) 276 | b[i>>3] += 1 << (i & 0x07) 277 | } 278 | } 279 | 280 | return b 281 | } 282 | 283 | //ExpandBool 展开布尔类型 284 | func ExpandBool(buf []byte, count int) []byte { 285 | length := len(buf) 286 | ln := length << 3 // length * 8 287 | if count > ln { 288 | count = ln 289 | } 290 | b := make([]byte, count) 291 | 292 | for i := 0; i < count; i++ { 293 | //b[i] = buf[i/8] & (1 << (i % 8)) 294 | if buf[i>>3]&(1<<(i&0x07)) > 0 { 295 | b[i] = 1 296 | } 297 | } 298 | 299 | return b 300 | } 301 | -------------------------------------------------------------------------------- /bin/checksum.go: -------------------------------------------------------------------------------- 1 | package bin 2 | 3 | //Sum 和 4 | func Sum(buf []byte) byte { 5 | var sum byte = 0 6 | l := len(buf) 7 | for i := 0; i < l; i++ { 8 | sum += buf[i] 9 | } 10 | return sum 11 | } 12 | 13 | //Xor 异或 14 | func Xor(buf []byte) byte { 15 | var xor byte = buf[0] 16 | l := len(buf) 17 | for i := 1; i < l; i++ { 18 | xor ^= buf[i] 19 | } 20 | return xor 21 | } 22 | -------------------------------------------------------------------------------- /bin/hex.go: -------------------------------------------------------------------------------- 1 | package bin 2 | 3 | import "encoding/hex" 4 | 5 | var hexNumbers = []byte("0123456789ABCDEF") 6 | 7 | //ByteToHex 编码 8 | func ByteToHex(value byte) []byte { 9 | buf := make([]byte, 2) 10 | buf[0] = hexNumbers[value>>4] 11 | buf[1] = hexNumbers[value&0x0F] 12 | return buf 13 | } 14 | 15 | //WriteByteHex 编码 16 | func WriteByteHex(buf []byte, value uint8) { 17 | buf[0] = hexNumbers[value>>4] 18 | buf[1] = hexNumbers[value&0x0F] 19 | } 20 | 21 | //WriteUint8Hex 编码 22 | func WriteUint8Hex(buf []byte, value uint8) { 23 | buf[0] = hexNumbers[value>>4] 24 | buf[1] = hexNumbers[value&0x0F] 25 | } 26 | 27 | //WriteUint16Hex 编码 28 | func WriteUint16Hex(buf []byte, value uint16) { 29 | h, l := value>>8, value&0xF 30 | buf[0] = hexNumbers[h>>4] 31 | buf[1] = hexNumbers[h&0x0F] 32 | buf[3] = hexNumbers[l>>4] 33 | buf[4] = hexNumbers[l&0x0F] 34 | 35 | } 36 | 37 | //ToHex 编码 38 | func ToHex(values []byte) []byte { 39 | length := len(values) 40 | buf := make([]byte, length<<1) //length * 2 41 | for i := 0; i < length; i++ { 42 | value := values[i] 43 | j := i << 1 //i * 2 44 | buf[j] = hexNumbers[value>>4] 45 | buf[j+1] = hexNumbers[value&0x0F] 46 | } 47 | return buf 48 | } 49 | 50 | //FromHex 解码 51 | func FromHex(values []byte) []byte { 52 | buf := make([]byte, len(values)>>1) //length / 2 53 | _, _ = hex.Decode(buf, values) 54 | return buf 55 | } 56 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 整体编译 4 | go env -w GOPROXY=https://goproxy.cn,direct 5 | go env -w GOPRIVATE=*.gitlab.com,*.gitee.com 6 | go env -w GOSUMDB=off 7 | 8 | app="iot-master" 9 | version="1.0.0" 10 | 11 | #npm run build 12 | 13 | 14 | pkg="github.com/busy-cloud/app" 15 | gitHash=$(git show -s --format=%H) 16 | buildTime=$(date -d today +"%Y-%m-%d %H:%M:%S") 17 | 18 | # -w -s 19 | ldflags="-X '${pkg}.Name=$name' \ 20 | -X '${pkg}.Version=$version' \ 21 | -X '${pkg}.GitHash=$gitHash' \ 22 | -X '${pkg}.Build=$buildTime'" 23 | 24 | #关闭CGO,如果使用了sqlite需要开启 25 | export CGO_ENABLED=0 26 | 27 | export GOARCH=amd64 28 | 29 | export GOOS=windows 30 | go build -ldflags "$ldflags" -o "${app}.exe" cmd/main.go 31 | 32 | export GOOS=linux 33 | go build -ldflags "$ldflags" -o "${app}" cmd/main.go 34 | 35 | -------------------------------------------------------------------------------- /calc/lang.go: -------------------------------------------------------------------------------- 1 | package calc 2 | 3 | import ( 4 | "github.com/PaesslerAG/gval" 5 | "github.com/spf13/cast" 6 | "math" 7 | ) 8 | 9 | func tarFunc(fun func(x float64) float64) func(x any) float64 { 10 | return func(x any) float64 { 11 | xx := cast.ToFloat64(x) 12 | return fun(xx) 13 | } 14 | } 15 | 16 | func tarFunc2(fun func(x, y float64) float64) func(x, y any) float64 { 17 | return func(x, y any) float64 { 18 | xx := cast.ToFloat64(x) 19 | yy := cast.ToFloat64(y) 20 | return fun(xx, yy) 21 | } 22 | } 23 | 24 | // 添加了数据函数的gval 25 | var lang = gval.Full( 26 | gval.Constant("E", math.E), 27 | gval.Constant("LN10", math.Ln10), 28 | gval.Constant("LN2", math.Ln2), 29 | gval.Constant("LOG10E", math.Log2E), 30 | gval.Constant("LOG2E", math.Log10E), 31 | gval.Constant("PI", math.Pi), 32 | //gval.Constant("PI", math.Sqrt2), 33 | //gval.Constant("PI", math.SqrtE), 34 | gval.Function("ABS", tarFunc(math.Abs)), 35 | gval.Function("CEIL", tarFunc(math.Ceil)), 36 | gval.Function("FLOOR", tarFunc(math.Floor)), 37 | gval.Function("TRUNC", tarFunc(math.Trunc)), 38 | gval.Function("POW", tarFunc2(math.Pow)), 39 | gval.Function("ROUND", tarFunc(math.Round)), 40 | gval.Function("SQRT", tarFunc(math.Sqrt)), 41 | gval.Function("CBRT", tarFunc(math.Cbrt)), 42 | gval.Function("EXP", tarFunc(math.Exp)), 43 | gval.Function("EXP2", tarFunc(math.Exp2)), 44 | gval.Function("EXPM1", tarFunc(math.Expm1)), 45 | gval.Function("SIN", tarFunc(math.Sin)), 46 | gval.Function("SINH", tarFunc(math.Sinh)), 47 | gval.Function("ASIN", tarFunc(math.Asin)), 48 | gval.Function("ASINH", tarFunc(math.Asinh)), 49 | gval.Function("COS", tarFunc(math.Cos)), 50 | gval.Function("COSH", tarFunc(math.Cosh)), 51 | gval.Function("ACOS", tarFunc(math.Acos)), 52 | gval.Function("ACOSH", tarFunc(math.Acosh)), 53 | gval.Function("TAN", tarFunc(math.Tan)), 54 | gval.Function("TANH", tarFunc(math.Tanh)), 55 | gval.Function("ATAN", tarFunc(math.Atan)), 56 | gval.Function("ATANH", tarFunc(math.Atanh)), 57 | gval.Function("LOG", tarFunc(math.Log)), 58 | gval.Function("LOG2", tarFunc(math.Log2)), 59 | gval.Function("LOG10", tarFunc(math.Log10)), 60 | gval.Function("LOG1p", tarFunc(math.Log1p)), 61 | gval.Function("HYPOT", tarFunc2(math.Hypot)), 62 | gval.Function("MAX", tarFunc2(math.Max)), 63 | gval.Function("MIN", tarFunc2(math.Min)), 64 | //gval.Function("RANDOM", func() {}), 65 | 66 | ) 67 | 68 | func Compile(expression string) (gval.Evaluable, error) { 69 | return lang.NewEvaluable(expression) 70 | } 71 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/busy-cloud/boat-ui" 5 | _ "github.com/busy-cloud/boat/apis" 6 | _ "github.com/busy-cloud/boat/apps" 7 | "github.com/busy-cloud/boat/boot" 8 | _ "github.com/busy-cloud/boat/broker" 9 | "github.com/busy-cloud/boat/log" 10 | "github.com/busy-cloud/boat/service" 11 | _ "github.com/busy-cloud/boat/table" 12 | "github.com/busy-cloud/boat/web" 13 | _ "github.com/busy-cloud/connector" 14 | _ "github.com/busy-cloud/dash" 15 | _ "github.com/busy-cloud/influxdb" 16 | _ "github.com/busy-cloud/modbus" 17 | _ "github.com/busy-cloud/noob" 18 | _ "github.com/busy-cloud/user" 19 | _ "github.com/god-jason/iot-master" //主程序 20 | "github.com/spf13/pflag" 21 | "github.com/spf13/viper" 22 | "os" 23 | "os/signal" 24 | "syscall" 25 | "time" 26 | ) 27 | 28 | func Startup() error { 29 | viper.SetConfigName("iot-master") 30 | 31 | err := boot.Startup() 32 | if err != nil { 33 | //_ = boot.Shutdown() 34 | return err 35 | } 36 | 37 | //异步执行,避免堵塞 38 | go func() { 39 | //启动服务 40 | err := web.Serve() 41 | if err != nil { 42 | //安全退出 43 | //_ = boot.Shutdown() 44 | log.Error(err) 45 | } 46 | }() 47 | 48 | log.Info("main started") 49 | 50 | return nil 51 | } 52 | 53 | func Shutdown() error { 54 | log.Info("main shutdown") 55 | 56 | return boot.Shutdown() 57 | } 58 | 59 | func main() { 60 | 61 | help := pflag.BoolP("help", "h", false, "show help") 62 | install := pflag.BoolP("install", "i", false, "install as service") 63 | uninstall := pflag.BoolP("uninstall", "u", false, "uninstall service") 64 | 65 | pflag.Parse() 66 | if *help { 67 | pflag.PrintDefaults() 68 | return 69 | } 70 | 71 | err := service.Register(Startup, Shutdown) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | if *install { 77 | log.Info("install service") 78 | err = service.Install() 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | } else if *uninstall { 83 | log.Info("uninstall service") 84 | err = service.Uninstall() 85 | if err != nil { 86 | log.Fatal(err) 87 | } 88 | return 89 | } 90 | 91 | sigs := make(chan os.Signal, 1) 92 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 93 | go func() { 94 | s := <-sigs 95 | log.Info("signal received ", s) 96 | 97 | //_ = boot.Shutdown() 98 | err := service.Stop() 99 | if err != nil { 100 | log.Error(err) 101 | } 102 | 103 | time.AfterFunc(10*time.Second, func() { 104 | os.Exit(0) 105 | }) 106 | }() 107 | 108 | err = service.Run() 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | 113 | println("bye") 114 | } 115 | -------------------------------------------------------------------------------- /device/device.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/db" 5 | "github.com/god-jason/iot-master/product" 6 | "time" 7 | ) 8 | 9 | func init() { 10 | db.Register(&Device{}, &DeviceModel{}) 11 | } 12 | 13 | type Device struct { 14 | Id string `json:"id,omitempty" xorm:"pk"` 15 | ProductId string `json:"product_id,omitempty" xorm:"index"` 16 | LinkerId string `json:"linker_id,omitempty" xorm:"index"` 17 | IncomingId string `json:"incoming_id,omitempty" xorm:"index"` 18 | Name string `json:"name,omitempty"` 19 | Description string `json:"description,omitempty"` 20 | Station map[string]any `json:"station,omitempty" xorm:"json"` //从站信息(协议定义表单) 21 | Disabled bool `json:"disabled,omitempty"` //禁用 22 | Created time.Time `json:"created,omitempty" xorm:"created"` 23 | } 24 | 25 | type DeviceModel struct { 26 | Id string `json:"id,omitempty" xorm:"pk"` 27 | Validators []*product.Validator `json:"validators,omitempty" xorm:"json"` 28 | Created time.Time `json:"created,omitempty" xorm:"created"` 29 | } 30 | -------------------------------------------------------------------------------- /device/device_api.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/api" 5 | "github.com/busy-cloud/boat/curd" 6 | "github.com/busy-cloud/boat/db" 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func init() { 11 | api.Register("GET", "iot/device/list", curd.ApiList[Device]()) 12 | api.Register("POST", "iot/device/search", curd.ApiSearch[Device]()) 13 | api.Register("POST", "iot/device/create", curd.ApiCreate[Device]()) 14 | api.Register("GET", "iot/device/:id", curd.ApiGet[Device]()) 15 | api.Register("POST", "iot/device/:id", curd.ApiUpdate[Device]("id", "name", "description", "product_id", "linker_id", "incoming_id", "disabled", "station")) 16 | api.Register("GET", "iot/device/:id/delete", curd.ApiDelete[Device]()) 17 | api.Register("GET", "iot/device/:id/enable", curd.ApiDisable[Device](false)) 18 | api.Register("GET", "iot/device/:id/disable", curd.ApiDisable[Device](true)) 19 | 20 | //物模型 21 | api.Register("GET", "iot/device/:id/model", curd.ApiGet[DeviceModel]()) 22 | api.Register("POST", "iot/device/:id/model", deviceModelUpdate) 23 | 24 | } 25 | 26 | func deviceModelUpdate(ctx *gin.Context) { 27 | id := ctx.Param("id") 28 | 29 | var model DeviceModel 30 | err := ctx.ShouldBind(&model) 31 | if err != nil { 32 | api.Error(ctx, err) 33 | return 34 | } 35 | model.Id = id 36 | 37 | _, err = db.Engine().ID(id).Delete(new(DeviceModel)) //不管有没有都删掉 38 | _, err = db.Engine().ID(id).Insert(&model) 39 | if err != nil { 40 | api.Error(ctx, err) 41 | return 42 | } 43 | 44 | api.OK(ctx, &model) 45 | } 46 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/god-jason/iot-master 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/PaesslerAG/gval v1.2.4 7 | github.com/busy-cloud/boat v0.5.2 8 | github.com/busy-cloud/boat-ui v0.5.0 9 | github.com/busy-cloud/connector v0.5.0 10 | github.com/busy-cloud/dash v0.5.0 11 | github.com/busy-cloud/influxdb v0.2.3 12 | github.com/busy-cloud/modbus v0.2.13 13 | github.com/busy-cloud/noob v0.5.0 14 | github.com/busy-cloud/user v0.5.0 15 | github.com/gin-gonic/gin v1.10.1 16 | github.com/spf13/cast v1.8.0 17 | github.com/spf13/pflag v1.0.6 18 | github.com/spf13/viper v1.20.1 19 | xorm.io/xorm v1.3.9 20 | ) 21 | 22 | require ( 23 | filippo.io/edwards25519 v1.1.0 // indirect 24 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect 25 | github.com/busy-cloud/serial v1.6.5 // indirect 26 | github.com/bytedance/sonic v1.13.2 // indirect 27 | github.com/bytedance/sonic/loader v0.2.4 // indirect 28 | github.com/cloudwego/base64x v0.1.5 // indirect 29 | github.com/ebitengine/purego v0.8.4 // indirect 30 | github.com/eclipse/paho.mqtt.golang v1.5.0 // indirect 31 | github.com/fsnotify/fsnotify v1.9.0 // indirect 32 | github.com/gabriel-vasile/mimetype v1.4.9 // indirect 33 | github.com/gin-contrib/cors v1.7.5 // indirect 34 | github.com/gin-contrib/gzip v1.2.3 // indirect 35 | github.com/gin-contrib/sessions v1.0.4 // indirect 36 | github.com/gin-contrib/sse v1.1.0 // indirect 37 | github.com/go-co-op/gocron/v2 v2.16.2 // indirect 38 | github.com/go-ole/go-ole v1.3.0 // indirect 39 | github.com/go-playground/locales v0.14.1 // indirect 40 | github.com/go-playground/universal-translator v0.18.1 // indirect 41 | github.com/go-playground/validator/v10 v10.26.0 // indirect 42 | github.com/go-sql-driver/mysql v1.9.2 // indirect 43 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 44 | github.com/goccy/go-json v0.10.5 // indirect 45 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 46 | github.com/golang/snappy v1.0.0 // indirect 47 | github.com/google/uuid v1.6.0 // indirect 48 | github.com/gorilla/context v1.1.2 // indirect 49 | github.com/gorilla/securecookie v1.1.2 // indirect 50 | github.com/gorilla/sessions v1.4.0 // indirect 51 | github.com/gorilla/websocket v1.5.3 // indirect 52 | github.com/influxdata/influxdb-client-go/v2 v2.14.0 // indirect 53 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect 54 | github.com/jonboulle/clockwork v0.5.0 // indirect 55 | github.com/json-iterator/go v1.1.12 // indirect 56 | github.com/kardianos/service v1.2.2 // indirect 57 | github.com/klauspost/cpuid/v2 v2.2.10 // indirect 58 | github.com/leodido/go-urn v1.4.0 // indirect 59 | github.com/lithammer/shortuuid v3.0.0+incompatible // indirect 60 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect 61 | github.com/mattn/go-isatty v0.0.20 // indirect 62 | github.com/mochi-mqtt/server/v2 v2.7.9 // indirect 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 64 | github.com/modern-go/reflect2 v1.0.2 // indirect 65 | github.com/oapi-codegen/runtime v1.0.0 // indirect 66 | github.com/panjf2000/ants/v2 v2.11.3 // indirect 67 | github.com/panjf2000/gnet/v2 v2.7.0 // indirect 68 | github.com/pelletier/go-toml/v2 v2.2.4 // indirect 69 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 70 | github.com/robfig/cron/v3 v3.0.1 // indirect 71 | github.com/rs/xid v1.6.0 // indirect 72 | github.com/sagikazarmark/locafero v0.9.0 // indirect 73 | github.com/segmentio/ksuid v1.0.4 // indirect 74 | github.com/shirou/gopsutil/v4 v4.25.4 // indirect 75 | github.com/shopspring/decimal v1.4.0 // indirect 76 | github.com/sirupsen/logrus v1.9.3 // indirect 77 | github.com/sourcegraph/conc v0.3.0 // indirect 78 | github.com/spf13/afero v1.14.0 // indirect 79 | github.com/subosito/gotenv v1.6.0 // indirect 80 | github.com/super-l/machine-code v0.0.0-20241121142923-4cb40646deba // indirect 81 | github.com/syndtr/goleveldb v1.0.0 // indirect 82 | github.com/tklauser/go-sysconf v0.3.15 // indirect 83 | github.com/tklauser/numcpus v0.10.0 // indirect 84 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 85 | github.com/ugorji/go/codec v1.2.14 // indirect 86 | github.com/valyala/bytebufferpool v1.0.0 // indirect 87 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 88 | github.com/zgwit/goselect v0.1.3 // indirect 89 | go.uber.org/multierr v1.11.0 // indirect 90 | go.uber.org/zap v1.27.0 // indirect 91 | golang.org/x/arch v0.17.0 // indirect 92 | golang.org/x/crypto v0.38.0 // indirect 93 | golang.org/x/net v0.40.0 // indirect 94 | golang.org/x/sync v0.14.0 // indirect 95 | golang.org/x/sys v0.33.0 // indirect 96 | golang.org/x/text v0.25.0 // indirect 97 | google.golang.org/protobuf v1.36.6 // indirect 98 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect 99 | gopkg.in/yaml.v3 v3.0.1 // indirect 100 | xorm.io/builder v0.3.13 // indirect 101 | ) 102 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 2 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 3 | gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= 4 | gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= 5 | github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU= 6 | github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= 7 | github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI= 8 | github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= 9 | github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= 10 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= 11 | github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= 12 | github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= 13 | github.com/busy-cloud/boat v0.5.2 h1:QUskZpuB6Fr8dPhTK3XM7avaDj2WoCS0CNzojfk9txc= 14 | github.com/busy-cloud/boat v0.5.2/go.mod h1:pArkdISo2peFMtlA61Cex6tftBZ7cXDqfgd/UKZ1zIU= 15 | github.com/busy-cloud/boat-ui v0.5.0 h1:yTmAtqjQNQJ4IKR5MBrnb6PzTuR9k0d+1aejW+k87LM= 16 | github.com/busy-cloud/boat-ui v0.5.0/go.mod h1:t9DwLmJyjbPHcm+IKzC9qUE7futO0pLigI9HIPbkhg0= 17 | github.com/busy-cloud/connector v0.5.0 h1:kydyvq+DozOmF+MkkVse8s8KI5xvL6DaYtA6vv1L6gE= 18 | github.com/busy-cloud/connector v0.5.0/go.mod h1:QKpr4iXrUj4nwjUkMxcy5buPQ8emJ8dvSYGuxstbkIA= 19 | github.com/busy-cloud/dash v0.5.0 h1:v7onTsSlRKKPN/OHnczyAfPL8+h0pIzrF6FHNh1IJT8= 20 | github.com/busy-cloud/dash v0.5.0/go.mod h1:YpLLCh50vwRy47NM9FFrAeg07Mh0qLxz6dzqYPJZm9c= 21 | github.com/busy-cloud/influxdb v0.2.3 h1:VW+5/q+/ClImAzrxwNT9zaXIbuHKch6rhKy9sJfOygg= 22 | github.com/busy-cloud/influxdb v0.2.3/go.mod h1:6mfMxknjGxBWcCOaLAkjig6YmssQSQbDPCeGeccCNPM= 23 | github.com/busy-cloud/modbus v0.2.13 h1:nFCxZLWwApU/sz4Nfy4mgQVgbUZDr8HdULFENDm8Ha4= 24 | github.com/busy-cloud/modbus v0.2.13/go.mod h1:voMqngF86wuY2p4BWf7wfI/5xsmDrRLuVBfjhYqijm8= 25 | github.com/busy-cloud/noob v0.5.0 h1:ejX2+YxRJbjbM5+gZeiwJ6htq5reEwAS+ip7b/OSci4= 26 | github.com/busy-cloud/noob v0.5.0/go.mod h1:G91PscNeLeOGag8xd6lsP7uEOOhzIfS26jMDam84kfY= 27 | github.com/busy-cloud/serial v1.6.5 h1:rDq99/cEDFUfZNJ0WmI/6iZz7LvmEDQcyw2gibhLI/E= 28 | github.com/busy-cloud/serial v1.6.5/go.mod h1:8IkXeVoYe424W0/giUH1Z2YTa9eqPDk/FtRcCu/7khI= 29 | github.com/busy-cloud/user v0.5.0 h1:Td5MdA9ichR9PCHp3o+FueezP/RW/TVXhYJE3/jGIEY= 30 | github.com/busy-cloud/user v0.5.0/go.mod h1:3d6+JNs3yy0DJXZAJwP7NLFMKpCMa6Ag/ACwCioirxg= 31 | github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= 32 | github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 33 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 34 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 35 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 36 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 37 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 38 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 39 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 41 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 42 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 43 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 44 | github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= 45 | github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= 46 | github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o= 47 | github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk= 48 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 49 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 50 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 51 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 52 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 53 | github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= 54 | github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= 55 | github.com/gin-contrib/cors v1.7.5 h1:cXC9SmofOrRg0w9PigwGlHG3ztswH6bqq4vJVXnvYMk= 56 | github.com/gin-contrib/cors v1.7.5/go.mod h1:4q3yi7xBEDDWKapjT2o1V7mScKDDr8k+jZ0fSquGoy0= 57 | github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U= 58 | github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c= 59 | github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= 60 | github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= 61 | github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= 62 | github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 63 | github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= 64 | github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 65 | github.com/go-co-op/gocron/v2 v2.16.2 h1:r08P663ikXiulLT9XaabkLypL/W9MoCIbqgQoAutyX4= 66 | github.com/go-co-op/gocron/v2 v2.16.2/go.mod h1:4YTLGCCAH75A5RlQ6q+h+VacO7CgjkgP0EJ+BEOXRSI= 67 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 68 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 69 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 70 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 71 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 72 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 73 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 74 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 75 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 76 | github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= 77 | github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 78 | github.com/go-sql-driver/mysql v1.9.2 h1:4cNKDYQ1I84SXslGddlsrMhc8k4LeDVj6Ad6WRjiHuU= 79 | github.com/go-sql-driver/mysql v1.9.2/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 80 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 81 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 82 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 83 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 84 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 85 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 86 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 87 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 88 | github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= 89 | github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 90 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 91 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 92 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 93 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 94 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 95 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 96 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 97 | github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= 98 | github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= 99 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 100 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 101 | github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= 102 | github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= 103 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 104 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 105 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 106 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 107 | github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4= 108 | github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI= 109 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= 110 | github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= 111 | github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= 112 | github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= 113 | github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= 114 | github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= 115 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 116 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 117 | github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= 118 | github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= 119 | github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= 120 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= 121 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 122 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 123 | github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= 124 | github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= 125 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 126 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 127 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 128 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 129 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 130 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 131 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 132 | github.com/lithammer/shortuuid v3.0.0+incompatible h1:NcD0xWW/MZYXEHa6ITy6kaXN5nwm/V115vj2YXfhS0w= 133 | github.com/lithammer/shortuuid v3.0.0+incompatible/go.mod h1:FR74pbAuElzOUuenUHTK2Tciko1/vKuIKS9dSkDrA4w= 134 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= 135 | github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= 136 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 137 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 138 | github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 139 | github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 140 | github.com/mochi-mqtt/server/v2 v2.7.9 h1:y0g4vrSLAag7T07l2oCzOa/+nKVLoazKEWAArwqBNYI= 141 | github.com/mochi-mqtt/server/v2 v2.7.9/go.mod h1:lZD3j35AVNqJL5cezlnSkuG05c0FCHSsfAKSPBOSbqc= 142 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 143 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 144 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 145 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 146 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 147 | github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= 148 | github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= 149 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 150 | github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= 151 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 152 | github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= 153 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 154 | github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= 155 | github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= 156 | github.com/panjf2000/gnet/v2 v2.7.0 h1:gR2fOhHHJ7Ds9tpqc2jxKgjFeaZmorTie5tDvqDAGEI= 157 | github.com/panjf2000/gnet/v2 v2.7.0/go.mod h1:HpNv+iQrIOeil1eyhdnKDlui7jivyMf0K3xwaeHKnh8= 158 | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= 159 | github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 160 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 161 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 162 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= 163 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 164 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= 165 | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 166 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 167 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 168 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 169 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 170 | github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= 171 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 172 | github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= 173 | github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= 174 | github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= 175 | github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= 176 | github.com/shirou/gopsutil/v4 v4.25.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw= 177 | github.com/shirou/gopsutil/v4 v4.25.4/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= 178 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 179 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 180 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 181 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 182 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 183 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 184 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 185 | github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= 186 | github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= 187 | github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= 188 | github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 189 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 190 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 191 | github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= 192 | github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= 193 | github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= 194 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 195 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 196 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 197 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 198 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 199 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 200 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 201 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 202 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 203 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 204 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 205 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 206 | github.com/super-l/machine-code v0.0.0-20241121142923-4cb40646deba h1:C5gpfEvyVK0LsmUr/Tcy0i2uBxSA2W77Hc7uVce+7LM= 207 | github.com/super-l/machine-code v0.0.0-20241121142923-4cb40646deba/go.mod h1:6oTChH5PcxwHUciRyeSdxJHeTLoMkiAg3gMjIounfZg= 208 | github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= 209 | github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= 210 | github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= 211 | github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= 212 | github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= 213 | github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= 214 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 215 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 216 | github.com/ugorji/go/codec v1.2.14 h1:yOQvXCBc3Ij46LRkRoh4Yd5qK6LVOgi0bYOXfb7ifjw= 217 | github.com/ugorji/go/codec v1.2.14/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 218 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 219 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 220 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 221 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 222 | github.com/zgwit/goselect v0.1.3 h1:0kAVui7agIqP2QchiBnuOHbeMMD+7z72Hmrd9A92Nx0= 223 | github.com/zgwit/goselect v0.1.3/go.mod h1:ygq9Kn15bCJer7uXny3lvsCFPHNnfFBgIOExd3ufUC8= 224 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 225 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 226 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 227 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 228 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 229 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 230 | golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= 231 | golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= 232 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 233 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 234 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 235 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 236 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 237 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 238 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 239 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 240 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 241 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 242 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 243 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 244 | golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 245 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 246 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 247 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 248 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 249 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 250 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 251 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 252 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 253 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 254 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= 255 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= 256 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 257 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 258 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 259 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 260 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 261 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 262 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 263 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= 264 | gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 265 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 266 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 267 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 268 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 269 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 270 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 271 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 272 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 273 | lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= 274 | lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= 275 | modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= 276 | modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= 277 | modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= 278 | modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= 279 | modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= 280 | modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= 281 | modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= 282 | modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= 283 | modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk= 284 | modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= 285 | modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= 286 | modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= 287 | modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE= 288 | modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= 289 | modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= 290 | modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= 291 | modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= 292 | modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 293 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 294 | xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo= 295 | xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= 296 | xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU= 297 | xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw= 298 | -------------------------------------------------------------------------------- /internal/boot.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/boot" 5 | _ "github.com/god-jason/iot-master/device" 6 | _ "github.com/god-jason/iot-master/product" 7 | _ "github.com/god-jason/iot-master/project" 8 | _ "github.com/god-jason/iot-master/protocol" 9 | _ "github.com/god-jason/iot-master/space" 10 | ) 11 | 12 | func init() { 13 | boot.Register("iot", &boot.Task{ 14 | Startup: Startup, 15 | Shutdown: nil, 16 | Depends: []string{"log", "mqtt", "database", "connector"}, 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /internal/device.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/db" 5 | "github.com/busy-cloud/boat/lib" 6 | "github.com/busy-cloud/boat/log" 7 | "github.com/busy-cloud/boat/mqtt" 8 | "github.com/god-jason/iot-master/device" 9 | "github.com/god-jason/iot-master/product" 10 | "github.com/god-jason/iot-master/project" 11 | "github.com/god-jason/iot-master/space" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | var devices lib.Map[Device] 17 | 18 | func GetDevice(id string) *Device { 19 | return devices.Load(id) 20 | } 21 | 22 | type Device struct { 23 | device.Device `xorm:"extends"` 24 | 25 | valuesLock sync.RWMutex 26 | Values map[string]any `json:"values"` 27 | Updated time.Time `json:"updated"` 28 | 29 | projects []string 30 | spaces []string 31 | 32 | validators []*Validator 33 | } 34 | 35 | func (d *Device) Open() error { 36 | d.Values = make(map[string]any) 37 | 38 | //查询绑定的项目 39 | var ps []*project.ProjectDevice 40 | err := db.Engine().Where("device_id=?", d.Id).Find(&ps) //.Distinct("project_id") 41 | if err != nil { 42 | return err 43 | } 44 | for _, p := range ps { 45 | d.projects = append(d.projects, p.ProjectId) 46 | } 47 | 48 | //查询绑定的设备 49 | var ss []*space.SpaceDevice 50 | err = db.Engine().Where("device_id=?", d.Id).Find(&ss) //.Distinct("space_id") 51 | if err != nil { 52 | return err 53 | } 54 | for _, s := range ss { 55 | d.spaces = append(d.spaces, s.SpaceId) 56 | } 57 | 58 | //加载产品物模型 59 | productModel, err := product.LoadModel(d.ProductId) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | //复制 65 | for _, v := range productModel.Validators { 66 | if v.Disabled { 67 | continue 68 | } 69 | vv := &Validator{Validator: v} 70 | d.validators = append(d.validators, vv) 71 | err = vv.Build() //重复编译了 72 | if err != nil { 73 | log.Error(err) 74 | } 75 | } 76 | 77 | //加载设备模型 78 | var deviceModel device.DeviceModel 79 | has, err := db.Engine().ID(d.Id).Get(&deviceModel) 80 | if err != nil { 81 | return err 82 | } 83 | if has { 84 | for _, v := range deviceModel.Validators { 85 | if v.Disabled { 86 | continue 87 | } 88 | vv := &Validator{Validator: v} 89 | d.validators = append(d.validators, vv) 90 | err = vv.Build() //重复编译了 91 | if err != nil { 92 | log.Error(err) 93 | } 94 | } 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (d *Device) PutValues(values map[string]any) { 101 | 102 | //TODO 过滤器实现 103 | 104 | //广播消息 105 | var topics []string 106 | topics = append(topics, "device/"+d.Id+"/values") 107 | for _, p := range d.projects { 108 | topics = append(topics, "project/"+p+"/device/"+d.Id+"/property") 109 | } 110 | for _, s := range d.spaces { 111 | topics = append(topics, "space/"+s+"/device/"+d.Id+"/property") 112 | } 113 | if len(topics) > 0 { 114 | mqtt.PublishEx(topics, values) 115 | } 116 | 117 | d.valuesLock.Lock() 118 | defer d.valuesLock.Unlock() //TODO 后续发消息和入库,锁的时间比较长 119 | 120 | //更新数据 121 | for k, v := range values { 122 | d.Values[k] = v 123 | } 124 | d.Values["__update"] = time.Now() 125 | 126 | //检查属性 127 | for _, v := range d.validators { 128 | alarm, err := v.Evaluate(d.Values) 129 | if err != nil { 130 | log.Error(err) 131 | } 132 | if alarm != nil { 133 | alarm.Device = d.Name 134 | alarm.DeviceId = d.Id 135 | 136 | var topics []string 137 | topics = append(topics, "device/"+d.Id+"/alarm") 138 | for _, p := range d.projects { 139 | alarm.ProjectId = p //TODO 多项目,会被覆盖掉 140 | topics = append(topics, "project/"+p+"/device/"+d.Id+"/alarm") 141 | } 142 | 143 | //入数据库 144 | _, err = db.Engine().InsertOne(alarm) 145 | if err != nil { 146 | log.Error(err) 147 | } 148 | 149 | mqtt.PublishEx(topics, alarm) 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /internal/device_api.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/api" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func init() { 9 | api.Register("GET", "iot/device/:id/values", deviceValues) 10 | } 11 | 12 | func deviceValues(ctx *gin.Context) { 13 | d := devices.Load(ctx.Param("id")) 14 | if d == nil { 15 | api.Fail(ctx, "device not found") 16 | return 17 | } 18 | api.OK(ctx, d.Values) 19 | } 20 | -------------------------------------------------------------------------------- /internal/mqtt.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/busy-cloud/boat/db" 6 | "github.com/busy-cloud/boat/log" 7 | "github.com/busy-cloud/boat/mqtt" 8 | "strings" 9 | ) 10 | 11 | func Startup() error { 12 | mqtt.Subscribe("device/+/+/property", func(topic string, payload []byte) { 13 | ss := strings.Split(topic, "/") 14 | id := ss[2] 15 | var values map[string]any 16 | err := json.Unmarshal(payload, &values) 17 | if err != nil { 18 | log.Error(err) 19 | return 20 | } 21 | 22 | d := devices.Load(id) 23 | if d == nil { 24 | d = &Device{} 25 | has, err := db.Engine().ID(id).Get(&d.Device) 26 | if err != nil { 27 | log.Error(err) 28 | return 29 | } 30 | if !has { 31 | log.Error("device not exist") 32 | return 33 | } 34 | err = d.Open() 35 | if err != nil { 36 | log.Error(err) 37 | } 38 | 39 | devices.Store(id, d) 40 | } 41 | 42 | d.PutValues(values) 43 | }) 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/validator.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/PaesslerAG/gval" 7 | "github.com/god-jason/iot-master/alarm" 8 | "github.com/god-jason/iot-master/calc" 9 | "github.com/god-jason/iot-master/product" 10 | "github.com/spf13/cast" 11 | "regexp" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | type Validator struct { 17 | *product.Validator 18 | 19 | expression gval.Evaluable 20 | 21 | start int64 22 | times int 23 | } 24 | 25 | func (v *Validator) Build() (err error) { 26 | if v.Type == "expression" && v.Expression != "" { 27 | v.expression, err = calc.Compile(v.Expression) 28 | } 29 | return err 30 | } 31 | 32 | func (v *Validator) Evaluate(ctx map[string]any) (*alarm.Alarm, error) { 33 | var err error 34 | var ret bool 35 | 36 | //检查条件 37 | switch v.Type { 38 | case "compare": 39 | ret, err = v.Compare.Evaluate(ctx) 40 | case "expression": 41 | if v.expression == nil { 42 | return nil, fmt.Errorf("invalid compare expression") 43 | } 44 | ret, err = v.expression.EvalBool(context.Background(), ctx) 45 | default: 46 | err = fmt.Errorf("unsupported validator type: %s", v.Type) 47 | } 48 | 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | //条件为真时,产生报警,所以条件为假,则重置 54 | if !ret { 55 | v.start = 0 56 | v.times = 0 57 | return nil, nil 58 | } 59 | 60 | //避免重复报警 61 | if v.times > 0 && v.Reset <= 0 { 62 | return nil, nil 63 | } 64 | 65 | //起始时间 66 | now := time.Now().Unix() 67 | if v.start == 0 { 68 | v.start = now 69 | } 70 | 71 | //延迟报警 72 | if v.Delay > 0 { 73 | if now < v.start+v.Delay { 74 | return nil, nil 75 | } 76 | } 77 | 78 | if v.times > 0 { 79 | //重复报警 80 | if v.Reset <= 0 { 81 | return nil, nil 82 | } 83 | 84 | //超过最大次数 85 | if v.ResetTimes > 0 && v.times > v.ResetTimes { 86 | return nil, nil 87 | } 88 | 89 | //还没到时间 90 | if now < v.start+v.Reset { 91 | return nil, nil 92 | } 93 | 94 | //重置开始时间 95 | v.start = now 96 | } 97 | 98 | v.times = v.times + 1 99 | 100 | //产生报警 101 | a := &alarm.Alarm{ 102 | Title: replaceParams(v.Title, ctx), 103 | Message: replaceParams(v.Message, ctx), 104 | Level: v.Level, 105 | } 106 | 107 | return a, nil 108 | } 109 | 110 | var paramsRegex *regexp.Regexp 111 | 112 | func init() { 113 | paramsRegex = regexp.MustCompile(`\{\w+\}`) 114 | } 115 | 116 | func replaceParams(str string, ctx map[string]any) string { 117 | return paramsRegex.ReplaceAllStringFunc(str, func(s string) string { 118 | s = strings.TrimPrefix(s, "{") 119 | s = strings.TrimSuffix(s, "}") 120 | return cast.ToString(ctx[s]) 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package iot_master 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | _ "github.com/busy-cloud/boat-ui" 7 | "github.com/busy-cloud/boat/apps" 8 | "github.com/busy-cloud/boat/log" 9 | "github.com/busy-cloud/boat/store" 10 | _ "github.com/god-jason/iot-master/internal" 11 | "github.com/god-jason/iot-master/protocol" 12 | ) 13 | 14 | //go:embed assets 15 | var assets embed.FS 16 | 17 | //go:embed pages 18 | var pages embed.FS 19 | 20 | //go:embed manifest.json 21 | var manifest []byte 22 | 23 | //go:embed protocols 24 | var protocols embed.FS 25 | 26 | func init() { 27 | //注册协议 28 | protocol.EmbedFS(&protocols, "protocols") 29 | 30 | //注册为内部插件 31 | var a apps.App 32 | err := json.Unmarshal(manifest, &a) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | apps.Register(&a) 37 | 38 | //注册资源 39 | a.AssetsFS = store.PrefixFS(&assets, "assets") 40 | a.PagesFS = store.PrefixFS(&pages, "pages") 41 | } 42 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "iot", 3 | "icon": "device.png", 4 | "name": "物联网", 5 | "description": "物联大师基础功能", 6 | "version": "v0.0.12", 7 | "shortcuts": [ 8 | { 9 | "name": "设备", 10 | "url": "page/iot/device", 11 | "icon": "device.png" 12 | }, 13 | { 14 | "name": "项目空间", 15 | "url": "page/iot/project", 16 | "icon": "project.png" 17 | }, 18 | { 19 | "name": "报警日志", 20 | "url": "page/iot/alarm", 21 | "icon": "alarm.png" 22 | }, 23 | { 24 | "name": "产品库", 25 | "url": "page/iot/product", 26 | "icon": "product.png" 27 | }, 28 | { 29 | "name": "协议库", 30 | "url": "page/iot/protocol", 31 | "icon": "protocol.png" 32 | } 33 | ], 34 | "menus": [ 35 | { 36 | "name": "物联网", 37 | "nz_icon": "global", 38 | "index": 200, 39 | "items": [ 40 | { 41 | "name": "设备", 42 | "url": "page/iot/device", 43 | "icon": "device.png" 44 | }, 45 | { 46 | "name": "项目空间", 47 | "url": "page/iot/project", 48 | "icon": "project.png" 49 | }, 50 | { 51 | "name": "报警日志", 52 | "url": "page/iot/alarm", 53 | "icon": "alarm.png" 54 | }, 55 | { 56 | "name": "产品库", 57 | "url": "page/iot/product", 58 | "icon": "product.png" 59 | }, 60 | { 61 | "name": "协议库", 62 | "url": "page/iot/protocol", 63 | "icon": "protocol.png" 64 | } 65 | ] 66 | } 67 | ], 68 | "pages": "pages" 69 | } -------------------------------------------------------------------------------- /pages/alarm.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "报警日志", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "key": "keyword", 7 | "type": "text", 8 | "placeholder": "请输入关键字" 9 | }, 10 | { 11 | "type": "button", 12 | "icon": "search", 13 | "label": "搜索", 14 | "action": { 15 | "type": "script", 16 | "script": "this.keyword=this.toolbar.value.keyword; this.search()" 17 | } 18 | } 19 | ], 20 | "keywords": [ 21 | "title", 22 | "message" 23 | ], 24 | "operators": [ 25 | { 26 | "icon": "delete", 27 | "title": "删除", 28 | "confirm": "确认删除?", 29 | "action": { 30 | "type": "script", 31 | "script": "this.request.get('iot/alarm/'+data.id+'/delete').subscribe(res=>{this.load()})" 32 | } 33 | } 34 | ], 35 | "columns": [ 36 | { 37 | "key": "id", 38 | "label": "ID" 39 | }, 40 | { 41 | "key": "device_id", 42 | "label": "设备ID", 43 | "action": { 44 | "type": "page", 45 | "page": "iot/device-detail", 46 | "params_func": "return {id: data.device_id}" 47 | } 48 | }, 49 | { 50 | "key": "project_id", 51 | "label": "项目ID", 52 | "action": { 53 | "type": "page", 54 | "page": "iot/project-detail", 55 | "params_func": "return {id: data.project_id}" 56 | } 57 | }, 58 | { 59 | "key": "title", 60 | "label": "标题" 61 | }, 62 | { 63 | "key": "message", 64 | "label": "消息" 65 | }, 66 | { 67 | "key": "level", 68 | "label": "等级" 69 | }, 70 | { 71 | "key": "created", 72 | "label": "日期", 73 | "type": "date" 74 | } 75 | ], 76 | "search_api": "iot/alarm/search", 77 | "mount": "if(this.params.device_id)this.filter.device_id=this.params.device_id;if(this.params.project_id)this.filter.project_id=this.params.project_id;" 78 | } -------------------------------------------------------------------------------- /pages/device-choose.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "设备", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "key": "keyword", 7 | "type": "text", 8 | "placeholder": "请输入关键字" 9 | }, 10 | { 11 | "type": "button", 12 | "icon": "search", 13 | "label": "搜索", 14 | "action": { 15 | "type": "script", 16 | "script": "this.keyword=this.toolbar.value.keyword; this.search()" 17 | } 18 | } 19 | ], 20 | "keywords": [ 21 | "id", 22 | "name", 23 | "description" 24 | ], 25 | "operators": [ 26 | { 27 | "icon": "check", 28 | "label": "选择", 29 | "action": { 30 | "type": "script", 31 | "script": "this.modelRef.close(data)" 32 | } 33 | } 34 | ], 35 | "columns": [ 36 | { 37 | "key": "id", 38 | "label": "ID" 39 | }, 40 | { 41 | "key": "name", 42 | "label": "名称" 43 | }, 44 | { 45 | "key": "description", 46 | "label": "说明" 47 | }, 48 | { 49 | "key": "station", 50 | "label": "地址", 51 | "type": "json" 52 | }, 53 | { 54 | "key": "disabled", 55 | "label": "禁用", 56 | "type": "boolean" 57 | }, 58 | { 59 | "key": "created", 60 | "label": "日期", 61 | "type": "date" 62 | } 63 | ], 64 | "search_api": "iot/device/search" 65 | } -------------------------------------------------------------------------------- /pages/device-create.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "创建设备", 3 | "template": "form", 4 | "toolbar": [ 5 | { 6 | "type": "button", 7 | "label": "选择产品ID", 8 | "action": { 9 | "type": "dialog", 10 | "page": "iot/product-choose", 11 | "after_close": "this.editor.patchValue({product_id: result.id})" 12 | } 13 | }, 14 | { 15 | "type": "button", 16 | "label": "选择连接器ID", 17 | "action": { 18 | "type": "dialog", 19 | "page": "connector/linker-choose", 20 | "after_close": "this.editor.patchValue({linker_id: result.id})" 21 | } 22 | }, 23 | { 24 | "type": "button", 25 | "label": "选择TCP连接器ID", 26 | "action": { 27 | "type": "dialog", 28 | "page": "connector/tcp-incoming-choose", 29 | "after_close": "this.editor.patchValue({incoming_id: result.id})" 30 | } 31 | } 32 | ], 33 | "fields": [ 34 | { 35 | "key": "id", 36 | "label": "ID", 37 | "type": "text", 38 | "placeholder": "默认随机ID" 39 | }, 40 | { 41 | "key": "name", 42 | "label": "名称", 43 | "type": "text" 44 | }, 45 | { 46 | "key": "description", 47 | "label": "说明", 48 | "type": "text" 49 | }, 50 | { 51 | "key": "product_id", 52 | "label": "产品ID", 53 | "type": "text", 54 | "change_action": { 55 | "type": "script", 56 | "script": "setTimeout(()=>this.load_product(), 100)" 57 | } 58 | }, 59 | { 60 | "key": "linker_id", 61 | "label": "连接器ID", 62 | "type": "text", 63 | "placeholder": "使用连接器时需要选择" 64 | }, 65 | { 66 | "key": "incoming_id", 67 | "label": "TCP连接器ID", 68 | "type": "text", 69 | "placeholder": "TCP服务器时需要选择" 70 | }, 71 | { 72 | "key": "station", 73 | "label": "地址", 74 | "type": "object", 75 | "placeholder": "需要先选择产品ID", 76 | "children": [] 77 | }, 78 | { 79 | "key": "disabled", 80 | "label": "禁用", 81 | "type": "switch" 82 | } 83 | ], 84 | "submit_api": "iot/device/create", 85 | "submit_success": "this.navigate('/page/iot/device-detail?id='+data.id)", 86 | "mount": "this.data.product_id=this.params.product_id; setTimeout(()=>this.load_product(), 100)", 87 | "methods": { 88 | "load_product": "this.editor.value.product_id && this.request.get('iot/product/'+this.editor.value.product_id).subscribe(res=>{if(!res.error) this.load_protocol_station(res.data.protocol)})", 89 | "load_protocol_station": ["p", "this.request.get('iot/protocol/'+p).subscribe(res=>{this.content.fields[6].children=res.station; setTimeout(()=>this.editor.rebuild(), 200)})"] 90 | } 91 | } -------------------------------------------------------------------------------- /pages/device-detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "设备详情", 3 | "template": "info", 4 | "toolbar": [ 5 | { 6 | "icon": "edit", 7 | "type": "button", 8 | "label": "编辑", 9 | "action": { 10 | "type": "page", 11 | "page": "iot/device-edit", 12 | "params_func": "return {id: data.id}" 13 | } 14 | }, 15 | { 16 | "icon": "delete", 17 | "type": "button", 18 | "label": "删除", 19 | "confirm": "确认删除?", 20 | "action": { 21 | "type": "script", 22 | "script": "this.request.get('iot/device/'+data.id+'/delete').subscribe(res=>{this.navigate('/page/iot/device')})" 23 | } 24 | }, 25 | { 26 | "icon": "build", 27 | "type": "button", 28 | "label": "设备物模型", 29 | "action": { 30 | "type": "page", 31 | "page": "iot/device-model", 32 | "params_func": "return {id: data.id}" 33 | } 34 | } 35 | ], 36 | "items": [ 37 | { 38 | "key": "id", 39 | "label": "ID" 40 | }, 41 | { 42 | "key": "name", 43 | "label": "名称" 44 | }, 45 | { 46 | "key": "description", 47 | "label": "说明" 48 | }, 49 | { 50 | "key": "product_id", 51 | "label": "产品ID", 52 | "action": { 53 | "type": "page", 54 | "page": "iot/product-detail", 55 | "params_func": "return {id: data.product_id}" 56 | } 57 | }, 58 | { 59 | "key": "linker_id", 60 | "label": "连接器ID", 61 | "type": "text", 62 | "action": { 63 | "type": "page", 64 | "page": "connector/linker-detail", 65 | "params_func": "return {id: data.linker_id}" 66 | } 67 | }, 68 | { 69 | "key": "incoming_id", 70 | "label": "TCP连接器ID", 71 | "type": "text", 72 | "action": { 73 | "type": "page", 74 | "page": "connector/tcp-incoming-detail", 75 | "params_func": "return {id: data.incoming_id}" 76 | } 77 | }, 78 | { 79 | "key": "station", 80 | "label": "地址", 81 | "type": "json" 82 | }, 83 | { 84 | "key": "disabled", 85 | "label": "禁用" 86 | } 87 | ], 88 | "load_api": "iot/device/:id", 89 | "tabs": [ 90 | { 91 | "title": "实时状态", 92 | "page": "iot/device-values", 93 | "params_func": "return {id: params.id}" 94 | }, 95 | { 96 | "title": "数据配置", 97 | "page": "iot/device-values-setting", 98 | "params_func": "return {id: params.id}" 99 | }, 100 | { 101 | "title": "设备告警", 102 | "page": "iot/alarm", 103 | "params_func": "return {device_id: params.id}" 104 | } 105 | ] 106 | } -------------------------------------------------------------------------------- /pages/device-edit.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "编辑设备", 3 | "template": "form", 4 | "toolbar": [ 5 | { 6 | "type": "button", 7 | "label": "选择产品ID", 8 | "action": { 9 | "type": "dialog", 10 | "page": "iot/product-choose", 11 | "after_close": "this.editor.patchValue({product_id: result.id})" 12 | } 13 | }, 14 | { 15 | "type": "button", 16 | "label": "选择连接器ID", 17 | "action": { 18 | "type": "dialog", 19 | "page": "connector/linker-choose", 20 | "after_close": "this.editor.patchValue({linker_id: result.id})" 21 | } 22 | }, 23 | { 24 | "type": "button", 25 | "label": "选择TCP连接器ID", 26 | "action": { 27 | "type": "dialog", 28 | "page": "connector/tcp-incoming-choose", 29 | "after_close": "this.editor.patchValue({incoming_id: result.id})" 30 | } 31 | } 32 | ], 33 | "fields": [ 34 | { 35 | "key": "id", 36 | "label": "ID", 37 | "type": "text" 38 | }, 39 | { 40 | "key": "name", 41 | "label": "名称", 42 | "type": "text" 43 | }, 44 | { 45 | "key": "description", 46 | "label": "说明", 47 | "type": "text" 48 | }, 49 | { 50 | "key": "product_id", 51 | "label": "产品ID", 52 | "type": "text", 53 | "change_action": { 54 | "type": "script", 55 | "script": "setTimeout(()=>this.load_product(), 100)" 56 | } 57 | }, 58 | { 59 | "key": "linker_id", 60 | "label": "连接器ID", 61 | "type": "text", 62 | "placeholder": "使用连接器时需要选择" 63 | }, 64 | { 65 | "key": "incoming_id", 66 | "label": "TCP连接器ID", 67 | "type": "text", 68 | "placeholder": "TCP服务器时需要选择" 69 | }, 70 | { 71 | "key": "station", 72 | "label": "地址", 73 | "type": "object", 74 | "placeholder": "需要先选择产品ID", 75 | "children": [] 76 | }, 77 | { 78 | "key": "disabled", 79 | "label": "禁用", 80 | "type": "switch" 81 | } 82 | ], 83 | "load_api": "iot/device/:id", 84 | "load_success": "setTimeout(()=>this.load_product(), 200)", 85 | "submit_api": "iot/device/:id", 86 | "submit_success": "this.navigate('/page/iot/device-detail?id='+data.id)", 87 | "mount": "", 88 | "methods": { 89 | "load_product": "this.editor.value.product_id && this.request.get('iot/product/'+this.editor.value.product_id).subscribe(res=>{if(!res.error) this.load_protocol_station(res.data.protocol)})", 90 | "load_protocol_station": ["p", "this.request.get('iot/protocol/'+p).subscribe(res=>{this.content.fields[6].children=res.station; setTimeout(()=>this.editor.rebuild(), 200)})"] 91 | } 92 | } -------------------------------------------------------------------------------- /pages/device-history.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "历史曲线", 3 | "template": "chart", 4 | "type": "line", 5 | "toolbar": [ 6 | { 7 | "key": "start", 8 | "type": "datetime", 9 | "label": "开始时间" 10 | }, 11 | { 12 | "key": "end", 13 | "type": "datetime", 14 | "label": "结束时间" 15 | }, 16 | { 17 | "key": "window", 18 | "type": "number", 19 | "default": "5", 20 | "label": "窗口" 21 | }, 22 | { 23 | "key": "unit", 24 | "type": "select", 25 | "default": "m", 26 | "options": [{ 27 | "value": "s", 28 | "label": "秒" 29 | },{ 30 | "value": "m", 31 | "label": "分钟" 32 | },{ 33 | "value": "h", 34 | "label": "小时" 35 | },{ 36 | "value": "d", 37 | "label": "天" 38 | }] 39 | }, 40 | { 41 | "key": "method", 42 | "type": "select", 43 | "label": "算子", 44 | "default": "last", 45 | "options": [{ 46 | "value": "last", 47 | "label": "最后值" 48 | },{ 49 | "value": "mean", 50 | "label": "均值" 51 | }] 52 | }, 53 | { 54 | "type": "button", 55 | "label": "查询", 56 | "action": { 57 | "type": "script", 58 | "script": "this.load_history()" 59 | } 60 | } 61 | ], 62 | "time": true, 63 | "options": { 64 | "tooltip": { 65 | "trigger": "axis" 66 | } 67 | }, 68 | "mount": "this.toolbarValue = {start:this.dayjs().subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss')}; setTimeout(()=>this.load_history(), 100)", 69 | "methods": { 70 | "load_history": "this.request.get('iot/device/'+this.params.id+'/history/'+this.params.point, {start:this.dayjs(this.toolbar.value.start).toISOString(), end:this.dayjs(this.toolbar.value.end).toISOString(), window:this.toolbar.value.window+this.toolbar.value.unit, method:this.toolbar.value.method}).subscribe(res=>{this.render(res.data.map(i=>{return [i.time, i.value]}))})" 71 | } 72 | } -------------------------------------------------------------------------------- /pages/device-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "导入设备", 3 | "template": "import", 4 | "columns": [ 5 | { 6 | "key": "id", 7 | "label": "ID", 8 | "type": "text" 9 | }, 10 | { 11 | "key": "name", 12 | "label": "名称", 13 | "type": "text" 14 | }, 15 | { 16 | "key": "description", 17 | "label": "说明", 18 | "type": "text" 19 | }, 20 | { 21 | "key": "product_id", 22 | "label": "产品ID", 23 | "type": "text", 24 | "change_action": { 25 | "type": "script", 26 | "script": "setTimeout(()=>this.load_product(), 100)" 27 | } 28 | }, 29 | { 30 | "key": "linker_id", 31 | "label": "连接器ID", 32 | "type": "text", 33 | "placeholder": "使用连接器时需要选择" 34 | }, 35 | { 36 | "key": "incoming_id", 37 | "label": "TCP连接器ID", 38 | "type": "text", 39 | "placeholder": "TCP服务器时需要选择" 40 | }, 41 | { 42 | "key": "station", 43 | "label": "地址", 44 | "type": "object", 45 | "placeholder": "需要先选择产品ID", 46 | "children": [] 47 | }, 48 | { 49 | "key": "disabled", 50 | "label": "禁用", 51 | "type": "switch" 52 | } 53 | ], 54 | "submit_api": "iot/device/:id" 55 | } -------------------------------------------------------------------------------- /pages/device-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "编辑设备物模型", 3 | "template": "form", 4 | "fields": [ 5 | { 6 | "key": "validators", 7 | "label": "属性检查", 8 | "type": "list", 9 | "children": [ 10 | { 11 | "key": "type", 12 | "label": "计算类型", 13 | "type": "radio", 14 | "default": "compare", 15 | "options": [ 16 | { 17 | "label": "表达式", 18 | "value": "expression" 19 | },{ 20 | "label": "比较器", 21 | "value": "compare" 22 | } 23 | ] 24 | }, 25 | { 26 | "key": "compare", 27 | "label": "比较器", 28 | "type": "object", 29 | "condition": { 30 | "key": "type", 31 | "type": "==", 32 | "value": "compare" 33 | }, 34 | "children": [ 35 | { 36 | "key": "name", 37 | "label": "属性(变量)", 38 | "type": "text" 39 | }, 40 | { 41 | "key": "type", 42 | "label": "对比", 43 | "type": "select", 44 | "default": "==", 45 | "options": [ 46 | { 47 | "label": "等于", 48 | "value": "==" 49 | }, 50 | { 51 | "label": "不等于", 52 | "value": "!=" 53 | }, 54 | { 55 | "label": "大于", 56 | "value": ">" 57 | }, 58 | { 59 | "label": "小于", 60 | "value": "<" 61 | }, 62 | { 63 | "label": "小于等于", 64 | "value": ">=" 65 | }, 66 | { 67 | "label": "小于等于", 68 | "value": "<=" 69 | } 70 | ] 71 | }, 72 | { 73 | "key": "value", 74 | "type": "number", 75 | "label": "值" 76 | } 77 | ] 78 | }, 79 | { 80 | "key": "expression", 81 | "label": "表达式", 82 | "type": "text", 83 | "condition": { 84 | "key": "type", 85 | "type": "==", 86 | "value": "expression" 87 | } 88 | }, 89 | { 90 | "key": "title", 91 | "label": "报警标题", 92 | "type": "text" 93 | }, 94 | { 95 | "key": "message", 96 | "label": "报警内容", 97 | "type": "text" 98 | }, 99 | { 100 | "key": "level", 101 | "label": "报警等级", 102 | "type": "select", 103 | "default": 3, 104 | "options": [ 105 | { 106 | "label": "一级", 107 | "value": 1 108 | }, 109 | { 110 | "label": "二级", 111 | "value": 2 112 | }, 113 | { 114 | "label": "三级", 115 | "value": 3 116 | }, 117 | { 118 | "label": "四级", 119 | "value": 4 120 | }, 121 | { 122 | "label": "五级", 123 | "value": 5 124 | } 125 | ] 126 | }, 127 | { 128 | "key": "delay", 129 | "type": "number", 130 | "label": "延迟报警s", 131 | "default": 60 132 | }, 133 | { 134 | "key": "reset", 135 | "type": "number", 136 | "label": "报警重置s", 137 | "default": 0 138 | }, 139 | { 140 | "key": "reset_times", 141 | "type": "number", 142 | "label": "报警重置次数", 143 | "default": 0 144 | }, 145 | { 146 | "key": "disabled", 147 | "label": "禁用", 148 | "type": "switch" 149 | } 150 | ] 151 | } 152 | ], 153 | "load_api": "iot/device/:id/model", 154 | "submit_api": "iot/device/:id/model", 155 | "submit_success": "this.navigate('/page/iot/device-detail?id='+data.id)", 156 | "methods": { 157 | } 158 | } -------------------------------------------------------------------------------- /pages/device-values-setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "设置属性", 3 | "template": "form", 4 | "fields": [ 5 | { 6 | "key": "key", 7 | "type": "select", 8 | "label": "属性", 9 | "options": [] 10 | },{ 11 | "key": "value", 12 | "type": "number", 13 | "label": "值" 14 | } 15 | ], 16 | "mount": "this.load_device()", 17 | "methods": { 18 | "load_device": "this.request.get('iot/device/'+this.params.id).subscribe(res=>{if(res.error)return; this.load_model(res.data.product_id)})", 19 | "load_model": ["pid","this.request.get('iot/product/'+pid+'/model').subscribe(res=>{if(res.error)return; this.content.fields[0].options=res.data.properties.map(p=>{return {value:p.name,label:p.label}}) })"] 20 | } 21 | } -------------------------------------------------------------------------------- /pages/device-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "实时状态", 3 | "template": "statistic", 4 | "toolbar": [ 5 | ], 6 | "items": [], 7 | "load_api": "iot/device/:id/values", 8 | "mount": "this.load_device(); this.value_action={type:'dialog',page:'iot/device-history',params_func:'return {id:this.params.id, point:data.key}'}", 9 | "methods": { 10 | "load_values": "this.request.get('iot/device/'+this.params.id+'/values').subscribe(res=>{if(res.error)return; this.data=res.data})", 11 | "load_device": "this.request.get('iot/device/'+this.params.id).subscribe(res=>{if(res.error)return; this.load_model(res.data.product_id)})", 12 | "load_model": ["pid","this.request.get('iot/product/'+pid+'/model').subscribe(res=>{if(res.error)return; this.content.items=res.data.properties.map(p=>{return{key:p.name,label:p.label,suffix:p.unit,span:6,action:this.value_action}}) })"] 13 | } 14 | } -------------------------------------------------------------------------------- /pages/device.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "设备", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "label": "创建", 7 | "icon": "plus", 8 | "type": "button", 9 | "action": { 10 | "type": "page", 11 | "page": "iot/device-create" 12 | } 13 | }, 14 | { 15 | "label": "导入", 16 | "icon": "plus", 17 | "type": "button", 18 | "action": { 19 | "type": "page", 20 | "page": "iot/device-import" 21 | } 22 | }, 23 | { 24 | "key": "keyword", 25 | "type": "text", 26 | "placeholder": "请输入关键字" 27 | }, 28 | { 29 | "type": "button", 30 | "icon": "search", 31 | "label": "搜索", 32 | "action": { 33 | "type": "script", 34 | "script": "this.keyword=this.toolbar.value.keyword; this.search()" 35 | } 36 | } 37 | ], 38 | "keywords": [ 39 | "id", 40 | "name", 41 | "description" 42 | ], 43 | "operators": [ 44 | { 45 | "icon": "eye", 46 | "action": { 47 | "type": "page", 48 | "page": "iot/device-detail", 49 | "params_func": "return {id: data.id}" 50 | } 51 | }, 52 | { 53 | "icon": "edit", 54 | "action": { 55 | "type": "page", 56 | "page": "iot/device-edit", 57 | "params_func": "return {id: data.id}" 58 | } 59 | }, 60 | { 61 | "icon": "delete", 62 | "title": "删除", 63 | "confirm": "确认删除?", 64 | "action": { 65 | "type": "script", 66 | "script": "this.request.get('iot/device/'+data.id+'/delete').subscribe(res=>{this.load()})" 67 | } 68 | } 69 | ], 70 | "columns": [ 71 | { 72 | "key": "id", 73 | "label": "ID", 74 | "action": { 75 | "type": "page", 76 | "page": "iot/device-detail", 77 | "params_func": "return {id: data.id}" 78 | } 79 | }, 80 | { 81 | "key": "product_id", 82 | "label": "产品ID", 83 | "action": { 84 | "type": "page", 85 | "page": "iot/product-detail", 86 | "params_func": "return {id: data.product_id}" 87 | } 88 | }, 89 | { 90 | "key": "name", 91 | "label": "名称" 92 | }, 93 | { 94 | "key": "description", 95 | "label": "说明" 96 | }, 97 | { 98 | "key": "station", 99 | "label": "地址", 100 | "type": "json" 101 | }, 102 | { 103 | "key": "disabled", 104 | "label": "禁用", 105 | "type": "boolean" 106 | }, 107 | { 108 | "key": "created", 109 | "label": "日期", 110 | "type": "date" 111 | } 112 | ], 113 | "search_api": "iot/device/search" 114 | } -------------------------------------------------------------------------------- /pages/license.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "授权协议", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "icon": "upload", 7 | "type": "upload", 8 | "upload": "/api/iot/license/upload" 9 | } 10 | ], 11 | "operators": [ 12 | { 13 | "icon": "download", 14 | "title": "下载", 15 | "action": { 16 | "type": "link", 17 | "external": true, 18 | "link": "/api/iot/license/:app_id/download" 19 | } 20 | } 21 | ], 22 | "columns": [ 23 | { 24 | "key": "app_id", 25 | "label": "应用" 26 | }, 27 | { 28 | "key": "owner", 29 | "label": "拥有者" 30 | }, 31 | { 32 | "key": "issuer", 33 | "label": "发行者" 34 | }, 35 | { 36 | "key": "issued", 37 | "label": "发布日期" 38 | }, 39 | { 40 | "key": "expire", 41 | "label": "失效日期" 42 | }, 43 | { 44 | "key": "cpuid", 45 | "label": "CPUID" 46 | }, 47 | { 48 | "key": "mac", 49 | "label": "网卡ID" 50 | }, 51 | { 52 | "key": "hosts", 53 | "label": "域名", 54 | "type": "tags" 55 | } 56 | ], 57 | "load_api": "iot/license/list" 58 | } -------------------------------------------------------------------------------- /pages/product-choose.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "产品", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "key": "keyword", 7 | "type": "text", 8 | "placeholder": "请输入关键字" 9 | }, 10 | { 11 | "type": "button", 12 | "icon": "search", 13 | "label": "搜索", 14 | "action": { 15 | "type": "script", 16 | "script": "this.keyword=this.toolbar.value.keyword; this.search()" 17 | } 18 | } 19 | ], 20 | "keywords": [ 21 | "id", 22 | "name", 23 | "description" 24 | ], 25 | "operators": [ 26 | { 27 | "icon": "check", 28 | "label": "选择", 29 | "action": { 30 | "type": "script", 31 | "script": "this.modelRef.close(data)" 32 | } 33 | } 34 | ], 35 | "columns": [ 36 | { 37 | "key": "id", 38 | "label": "ID" 39 | }, 40 | { 41 | "key": "name", 42 | "label": "名称" 43 | }, 44 | { 45 | "key": "version", 46 | "label": "版本" 47 | } 48 | ], 49 | "search_api": "iot/product/search" 50 | } -------------------------------------------------------------------------------- /pages/product-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "编辑协议配置", 3 | "template": "form", 4 | "fields": [ 5 | ], 6 | "load_api": "iot/product/:product/config/:config", 7 | "submit_api": "iot/product/:product/config/:config", 8 | "submit_success": "this.navigate('/page/iot/product-detail?id='+data.id)", 9 | "mount": "this.load_config_form()", 10 | "methods": { 11 | "load_config_form": "this.request.get('iot/protocol/'+this.params.config).subscribe(res=>{this.content.fields = res.model})" 12 | } 13 | } -------------------------------------------------------------------------------- /pages/product-create.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "创建产品", 3 | "template": "form", 4 | "fields": [ 5 | { 6 | "key": "id", 7 | "label": "ID", 8 | "type": "text", 9 | "required": true 10 | }, 11 | { 12 | "key": "name", 13 | "label": "名称", 14 | "type": "text", 15 | "required": true 16 | }, 17 | { 18 | "key": "description", 19 | "label": "说明", 20 | "type": "text" 21 | }, 22 | { 23 | "key": "type", 24 | "label": "类型", 25 | "type": "text" 26 | }, 27 | { 28 | "key": "version", 29 | "label": "版本", 30 | "type": "text" 31 | }, 32 | { 33 | "key": "protocol", 34 | "label": "协议", 35 | "type": "select", 36 | "options": [] 37 | }, 38 | { 39 | "key": "disabled", 40 | "label": "禁用", 41 | "type": "switch" 42 | } 43 | ], 44 | "submit_api": "iot/product/create", 45 | "submit_success": "this.navigate('/page/iot/product-detail?id='+data.id)", 46 | "mount": "this.load_protocols()", 47 | "methods": { 48 | "load_protocols": "this.request.get('iot/protocol/list').subscribe(res=>{this.content.fields[5].options=res.data.map(d=>{return {value:d.name,label:d.description}}); })" 49 | } 50 | } -------------------------------------------------------------------------------- /pages/product-detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "产品详情", 3 | "template": "info", 4 | "toolbar": [ 5 | { 6 | "icon": "edit", 7 | "type": "button", 8 | "label": "编辑", 9 | "action": { 10 | "type": "page", 11 | "page": "iot/product-edit", 12 | "params_func": "return {id: data.id}" 13 | } 14 | }, 15 | { 16 | "icon": "api", 17 | "type": "button", 18 | "label": "产品协议配置", 19 | "action": { 20 | "type": "page", 21 | "page": "iot/product-config", 22 | "params_func": "return {product: data.id, config: data.protocol}" 23 | } 24 | }, 25 | { 26 | "icon": "delete", 27 | "type": "button", 28 | "label": "删除", 29 | "confirm": "确认删除?", 30 | "action": { 31 | "type": "script", 32 | "script": "this.request.get('iot/product/'+data.id+'/delete').subscribe(res=>{this.navigate('/page/iot/product')})" 33 | } 34 | } 35 | ], 36 | "items": [ 37 | { 38 | "key": "id", 39 | "label": "ID" 40 | }, 41 | { 42 | "key": "name", 43 | "label": "名称" 44 | }, 45 | { 46 | "key": "description", 47 | "label": "说明" 48 | }, 49 | { 50 | "key": "type", 51 | "label": "类型" 52 | }, 53 | { 54 | "key": "version", 55 | "label": "版本" 56 | }, 57 | { 58 | "key": "protocol", 59 | "label": "协议" 60 | }, 61 | { 62 | "key": "disabled", 63 | "label": "禁用", 64 | "type": "boolean" 65 | } 66 | ], 67 | "load_api": "iot/product/:id", 68 | "tabs": [ 69 | { 70 | "title": "产品设备", 71 | "page": "iot/product-device", 72 | "params_func": "return {product_id: params.id}" 73 | }, 74 | { 75 | "title": "产品物模型", 76 | "page": "iot/product-model", 77 | "params_func": "return {id: params.id}" 78 | } 79 | ] 80 | } -------------------------------------------------------------------------------- /pages/product-device.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "产品设备", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "label": "创建", 7 | "icon": "plus", 8 | "type": "button", 9 | "action": { 10 | "type": "page", 11 | "page": "iot/device-create", 12 | "params_func": "return {product_id: this.params.product_id}" 13 | } 14 | }, 15 | { 16 | "key": "keyword", 17 | "type": "text", 18 | "placeholder": "请输入关键字" 19 | }, 20 | { 21 | "type": "button", 22 | "icon": "search", 23 | "label": "搜索", 24 | "action": { 25 | "type": "script", 26 | "script": "this.keyword=this.toolbar.value.keyword; this.search()" 27 | } 28 | } 29 | ], 30 | "keywords": [ 31 | "id", 32 | "name", 33 | "description" 34 | ], 35 | "operators": [ 36 | { 37 | "icon": "eye", 38 | "action": { 39 | "type": "page", 40 | "page": "iot/device-detail", 41 | "params_func": "return {id: data.id}" 42 | } 43 | }, 44 | { 45 | "icon": "edit", 46 | "action": { 47 | "type": "page", 48 | "page": "iot/device-edit", 49 | "params_func": "return {id: data.id}" 50 | } 51 | }, 52 | { 53 | "icon": "delete", 54 | "title": "删除", 55 | "confirm": "确认删除?", 56 | "action": { 57 | "type": "script", 58 | "script": "this.request.get('iot/device/'+data.id+'/delete').subscribe(res=>{this.load()})" 59 | } 60 | } 61 | ], 62 | "columns": [ 63 | { 64 | "key": "id", 65 | "label": "ID", 66 | "action": { 67 | "type": "page", 68 | "page": "iot/device-detail", 69 | "params_func": "return {id: data.id}" 70 | } 71 | }, 72 | { 73 | "key": "product_id", 74 | "label": "产品ID", 75 | "action": { 76 | "type": "page", 77 | "page": "iot/product-detail", 78 | "params_func": "return {id: data.product_id}" 79 | } 80 | }, 81 | { 82 | "key": "name", 83 | "label": "名称" 84 | }, 85 | { 86 | "key": "description", 87 | "label": "说明" 88 | }, 89 | { 90 | "key": "station", 91 | "label": "地址", 92 | "type": "json" 93 | }, 94 | { 95 | "key": "disabled", 96 | "label": "禁用", 97 | "type": "boolean" 98 | }, 99 | { 100 | "key": "created", 101 | "label": "日期", 102 | "type": "date" 103 | } 104 | ], 105 | "search_api": "iot/device/search", 106 | "mount": "if(this.params.product_id)this.filter.product_id=this.params.product_id" 107 | } -------------------------------------------------------------------------------- /pages/product-edit.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "编辑产品", 3 | "template": "form", 4 | "fields": [ 5 | { 6 | "key": "id", 7 | "label": "ID", 8 | "type": "text", 9 | "required": true 10 | }, 11 | { 12 | "key": "name", 13 | "label": "名称", 14 | "type": "text", 15 | "required": true 16 | }, 17 | { 18 | "key": "description", 19 | "label": "说明", 20 | "type": "text" 21 | }, 22 | { 23 | "key": "type", 24 | "label": "类型", 25 | "type": "text" 26 | }, 27 | { 28 | "key": "version", 29 | "label": "版本", 30 | "type": "text" 31 | }, 32 | { 33 | "key": "protocol", 34 | "label": "协议", 35 | "type": "select", 36 | "options": [] 37 | }, 38 | { 39 | "key": "disabled", 40 | "label": "禁用", 41 | "type": "switch" 42 | } 43 | ], 44 | "load_api": "iot/product/:id", 45 | "submit_api": "iot/product/:id", 46 | "submit_success": "this.navigate('/page/iot/product-detail?id='+data.id)", 47 | "mount": "this.load_protocols()", 48 | "methods": { 49 | "load_protocols": "this.request.get('iot/protocol/list').subscribe(res=>{this.content.fields[5].options=res.data.map(d=>{return {value:d.name,label:d.description}}); })" 50 | } 51 | } -------------------------------------------------------------------------------- /pages/product-model.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "编辑产品物模型", 3 | "template": "form", 4 | "fields": [ 5 | { 6 | "key": "properties", 7 | "label": "属性表", 8 | "type": "table", 9 | "children": [ 10 | { 11 | "key": "name", 12 | "label": "变量", 13 | "type": "text" 14 | }, 15 | { 16 | "key": "label", 17 | "label": "显示名称", 18 | "type": "text" 19 | }, 20 | { 21 | "key": "unit", 22 | "label": "数据单位", 23 | "type": "text" 24 | }, 25 | { 26 | "key": "type", 27 | "label": "数据类型", 28 | "type": "select", 29 | "default": "number", 30 | "options": [ 31 | { 32 | "label": "布尔", 33 | "value": "bool" 34 | }, 35 | { 36 | "label": "字符串", 37 | "value": "string" 38 | }, 39 | { 40 | "label": "数值", 41 | "value": "number" 42 | }, 43 | { 44 | "label": "数组", 45 | "value": "array" 46 | }, 47 | { 48 | "label": "对象", 49 | "value": "object" 50 | } 51 | ] 52 | }, 53 | { 54 | "key": "precision", 55 | "label": "精度", 56 | "type": "number", 57 | "default": 0 58 | } 59 | ] 60 | }, 61 | { 62 | "key": "validators", 63 | "label": "属性检查", 64 | "type": "list", 65 | "children": [ 66 | { 67 | "key": "type", 68 | "label": "计算类型", 69 | "type": "radio", 70 | "default": "compare", 71 | "options": [ 72 | { 73 | "label": "表达式", 74 | "value": "expression" 75 | },{ 76 | "label": "比较器", 77 | "value": "compare" 78 | } 79 | ] 80 | }, 81 | { 82 | "key": "compare", 83 | "label": "比较器", 84 | "type": "object", 85 | "condition": { 86 | "key": "type", 87 | "type": "==", 88 | "value": "compare" 89 | }, 90 | "children": [ 91 | { 92 | "key": "name", 93 | "label": "属性(变量)", 94 | "type": "text" 95 | }, 96 | { 97 | "key": "type", 98 | "label": "对比", 99 | "type": "select", 100 | "default": "==", 101 | "options": [ 102 | { 103 | "label": "等于", 104 | "value": "==" 105 | }, 106 | { 107 | "label": "不等于", 108 | "value": "!=" 109 | }, 110 | { 111 | "label": "大于", 112 | "value": ">" 113 | }, 114 | { 115 | "label": "小于", 116 | "value": "<" 117 | }, 118 | { 119 | "label": "小于等于", 120 | "value": ">=" 121 | }, 122 | { 123 | "label": "小于等于", 124 | "value": "<=" 125 | } 126 | ] 127 | }, 128 | { 129 | "key": "value", 130 | "type": "number", 131 | "label": "值" 132 | } 133 | ] 134 | }, 135 | { 136 | "key": "expression", 137 | "label": "表达式", 138 | "type": "text", 139 | "condition": { 140 | "key": "type", 141 | "type": "==", 142 | "value": "expression" 143 | } 144 | }, 145 | { 146 | "key": "title", 147 | "label": "报警标题", 148 | "type": "text" 149 | }, 150 | { 151 | "key": "message", 152 | "label": "报警内容", 153 | "type": "text" 154 | }, 155 | { 156 | "key": "level", 157 | "label": "报警等级", 158 | "type": "select", 159 | "default": 3, 160 | "options": [ 161 | { 162 | "label": "一级", 163 | "value": 1 164 | }, 165 | { 166 | "label": "二级", 167 | "value": 2 168 | }, 169 | { 170 | "label": "三级", 171 | "value": 3 172 | }, 173 | { 174 | "label": "四级", 175 | "value": 4 176 | }, 177 | { 178 | "label": "五级", 179 | "value": 5 180 | } 181 | ] 182 | }, 183 | { 184 | "key": "delay", 185 | "type": "number", 186 | "label": "延迟报警s", 187 | "default": 60 188 | }, 189 | { 190 | "key": "reset", 191 | "type": "number", 192 | "label": "报警重置s", 193 | "default": 0 194 | }, 195 | { 196 | "key": "reset_times", 197 | "type": "number", 198 | "label": "报警重置次数", 199 | "default": 0 200 | }, 201 | { 202 | "key": "disabled", 203 | "label": "禁用", 204 | "type": "switch" 205 | } 206 | ] 207 | } 208 | ], 209 | "load_api": "iot/product/:id/model", 210 | "submit_api": "iot/product/:id/model", 211 | "submit_success": "this.navigate('/page/iot/product-detail?id='+data.id)", 212 | "methods": { 213 | } 214 | } -------------------------------------------------------------------------------- /pages/product.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "产品", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "label": "创建", 7 | "icon": "plus", 8 | "type": "button", 9 | "action": { 10 | "type": "page", 11 | "page": "iot/product-create" 12 | } 13 | }, 14 | { 15 | "key": "keyword", 16 | "type": "text", 17 | "placeholder": "请输入关键字" 18 | }, 19 | { 20 | "type": "button", 21 | "icon": "search", 22 | "label": "搜索", 23 | "action": { 24 | "type": "script", 25 | "script": "this.keyword=this.toolbar.value.keyword; this.search()" 26 | } 27 | } 28 | ], 29 | "keywords": [ 30 | "id", 31 | "name", 32 | "description" 33 | ], 34 | "operators": [ 35 | { 36 | "icon": "eye", 37 | "action": { 38 | "type": "page", 39 | "page": "iot/product-detail", 40 | "params_func": "return {id: data.id}" 41 | } 42 | }, 43 | { 44 | "icon": "edit", 45 | "action": { 46 | "type": "page", 47 | "page": "iot/product-edit", 48 | "params_func": "return {id: data.id}" 49 | } 50 | }, 51 | { 52 | "icon": "delete", 53 | "title": "删除", 54 | "confirm": "确认删除?", 55 | "action": { 56 | "type": "script", 57 | "script": "this.request.get('iot/product/'+data.id+'/delete').subscribe(res=>{this.load()})" 58 | } 59 | } 60 | ], 61 | "columns": [ 62 | { 63 | "key": "id", 64 | "label": "ID", 65 | "action": { 66 | "type": "page", 67 | "page": "iot/product-detail", 68 | "params_func": "return {id: data.id}" 69 | } 70 | }, 71 | { 72 | "key": "name", 73 | "label": "名称" 74 | }, 75 | { 76 | "key": "description", 77 | "label": "说明" 78 | }, 79 | { 80 | "key": "type", 81 | "label": "类型" 82 | }, 83 | { 84 | "key": "version", 85 | "label": "版本" 86 | }, 87 | { 88 | "key": "protocol", 89 | "label": "协议" 90 | }, 91 | { 92 | "key": "disabled", 93 | "label": "禁用", 94 | "type": "boolean" 95 | }, 96 | { 97 | "key": "created", 98 | "label": "日期", 99 | "type": "date" 100 | } 101 | ], 102 | "search_api": "iot/product/search" 103 | } -------------------------------------------------------------------------------- /pages/project-choose.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "项目", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "key": "keyword", 7 | "type": "text", 8 | "placeholder": "请输入关键字" 9 | }, 10 | { 11 | "type": "button", 12 | "icon": "search", 13 | "label": "搜索", 14 | "action": { 15 | "type": "script", 16 | "script": "this.keyword=this.toolbar.value.keyword; this.search()" 17 | } 18 | } 19 | ], 20 | "keywords": [ 21 | "id", 22 | "name", 23 | "description" 24 | ], 25 | "operators": [ 26 | { 27 | "icon": "check", 28 | "label": "选择", 29 | "action": { 30 | "type": "script", 31 | "script": "this.modelRef.close(data)" 32 | } 33 | } 34 | ], 35 | "columns": [ 36 | { 37 | "key": "id", 38 | "label": "ID" 39 | }, 40 | { 41 | "key": "name", 42 | "label": "名称" 43 | }, 44 | { 45 | "key": "description", 46 | "label": "说明" 47 | } 48 | ], 49 | "search_api": "iot/project/search" 50 | } -------------------------------------------------------------------------------- /pages/project-create.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "创建项目", 3 | "template": "form", 4 | "fields": [ 5 | { 6 | "key": "id", 7 | "label": "ID", 8 | "type": "text", 9 | "required": true 10 | }, 11 | { 12 | "key": "name", 13 | "label": "名称", 14 | "type": "text", 15 | "required": true 16 | }, 17 | { 18 | "key": "description", 19 | "label": "说明", 20 | "type": "text" 21 | }, 22 | { 23 | "key": "disabled", 24 | "label": "禁用", 25 | "type": "switch" 26 | } 27 | ], 28 | "submit_api": "iot/project/create", 29 | "submit_success": "this.navigate('/page/iot/project-detail?id='+data.id)", 30 | "methods": { 31 | } 32 | } -------------------------------------------------------------------------------- /pages/project-detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "项目详情", 3 | "template": "info", 4 | "toolbar": [ 5 | { 6 | "icon": "edit", 7 | "type": "button", 8 | "label": "编辑", 9 | "action": { 10 | "type": "page", 11 | "page": "iot/project-edit", 12 | "params_func": "return {id: data.id}" 13 | } 14 | }, 15 | { 16 | "icon": "delete", 17 | "type": "button", 18 | "label": "删除", 19 | "confirm": "确认删除?", 20 | "action": { 21 | "type": "script", 22 | "script": "this.request.get('iot/project/'+data.id+'/delete').subscribe(res=>{this.navigate('/page/iot/project')})" 23 | } 24 | } 25 | ], 26 | "items": [ 27 | { 28 | "key": "id", 29 | "label": "ID" 30 | }, 31 | { 32 | "key": "name", 33 | "label": "名称" 34 | }, 35 | { 36 | "key": "description", 37 | "label": "说明" 38 | }, 39 | { 40 | "key": "disabled", 41 | "label": "禁用" 42 | } 43 | ], 44 | "load_api": "iot/project/:id", 45 | "tabs": [ 46 | { 47 | "title": "项目空间", 48 | "page": "iot/space", 49 | "params_func": "return {project_id: params.id}" 50 | }, 51 | { 52 | "title": "项目设备", 53 | "page": "iot/project-device", 54 | "params_func": "return {project_id: params.id}" 55 | }, 56 | { 57 | "title": "项目报警", 58 | "page": "iot/alarm", 59 | "params_func": "return {project_id: params.id}" 60 | }, 61 | { 62 | "title": "项目用户", 63 | "page": "iot/project-user", 64 | "params_func": "return {project_id: params.id}" 65 | }, 66 | { 67 | "title": "项目插件", 68 | "page": "iot/project-plugin", 69 | "params_func": "return {project_id: params.id}" 70 | } 71 | ] 72 | } -------------------------------------------------------------------------------- /pages/project-device-choose.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "项目设备", 3 | "template": "table", 4 | "operators": [ 5 | { 6 | "icon": "check", 7 | "label": "选择", 8 | "action": { 9 | "type": "script", 10 | "script": "this.modelRef.close(data)" 11 | } 12 | } 13 | ], 14 | "columns": [ 15 | { 16 | "key": "device_id", 17 | "label": "设备ID" 18 | }, 19 | { 20 | "key": "name", 21 | "label": "名称" 22 | }, 23 | { 24 | "key": "created", 25 | "label": "日期", 26 | "type": "date" 27 | } 28 | ], 29 | "load_api": "iot/project/:project_id/device/list?limit=99999" 30 | } -------------------------------------------------------------------------------- /pages/project-device.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "项目设备", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "label": "绑定", 7 | "icon": "plus", 8 | "type": "button", 9 | "action": { 10 | "type": "dialog", 11 | "page": "iot/device-choose", 12 | "after_close": "this.request.get('iot/project/'+this.params.project_id+'/device/'+result.id+'/bind').subscribe(res=>this.load())" 13 | } 14 | } 15 | ], 16 | "operators": [ 17 | { 18 | "icon": "eye", 19 | "action": { 20 | "type": "page", 21 | "page": "iot/device-detail", 22 | "params_func": "return {id: data.id}" 23 | } 24 | }, 25 | { 26 | "icon": "edit", 27 | "action": { 28 | "type": "page", 29 | "page": "iot/device-edit", 30 | "params_func": "return {id: data.id}" 31 | } 32 | }, 33 | { 34 | "icon": "delete", 35 | "title": "解绑", 36 | "confirm": "确认解绑?", 37 | "action": { 38 | "type": "script", 39 | "script": "this.request.get('iot/project/'+data.project_id+'/device/'+data.device_id+'/unbind').subscribe(res=>this.load())" 40 | } 41 | } 42 | ], 43 | "columns": [ 44 | { 45 | "key": "device_id", 46 | "label": "设备ID", 47 | "action": { 48 | "type": "page", 49 | "page": "iot/device-detail", 50 | "params_func": "return {id: data.device_id}" 51 | } 52 | }, 53 | { 54 | "key": "name", 55 | "label": "名称" 56 | }, 57 | { 58 | "key": "created", 59 | "label": "日期", 60 | "type": "date" 61 | } 62 | ], 63 | "load_api": "iot/project/:project_id/device/list?limit=99999" 64 | } -------------------------------------------------------------------------------- /pages/project-edit.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "编辑项目", 3 | "template": "form", 4 | "fields": [ 5 | { 6 | "key": "id", 7 | "label": "ID", 8 | "type": "text", 9 | "required": true 10 | }, 11 | { 12 | "key": "name", 13 | "label": "名称", 14 | "type": "text", 15 | "required": true 16 | }, 17 | { 18 | "key": "description", 19 | "label": "说明", 20 | "type": "text" 21 | }, 22 | { 23 | "key": "disabled", 24 | "label": "禁用", 25 | "type": "switch" 26 | } 27 | ], 28 | "load_api": "iot/project/:id", 29 | "submit_api": "iot/project/:id", 30 | "submit_success": "this.navigate('/page/iot/project-detail?id='+data.id)", 31 | "methods": { 32 | } 33 | } -------------------------------------------------------------------------------- /pages/project-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "项目应用", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "label": "绑定", 7 | "icon": "plus", 8 | "type": "button", 9 | "action": { 10 | "type": "dialog", 11 | "page": "plugin/plugin-choose", 12 | "after_close": "this.request.get('iot/project/'+this.params.project_id+'/plugin/'+result.id+'/bind').subscribe(res=>this.load())" 13 | } 14 | } 15 | ], 16 | "operators": [ 17 | { 18 | "icon": "select", 19 | "title": "打开", 20 | "action": { 21 | "type": "script", 22 | "script": "window.open('/plugin/'+data.app_id+'?project='+this.params.project_id)" 23 | } 24 | }, 25 | { 26 | "icon": "delete", 27 | "title": "解绑", 28 | "confirm": "确认解绑?", 29 | "action": { 30 | "type": "script", 31 | "script": "this.request.get('iot/project/'+data.project_id+'/plugin/'+data.app_id+'/unbind').subscribe(res=>this.load())" 32 | } 33 | } 34 | ], 35 | "columns": [ 36 | { 37 | "key": "icon", 38 | "label": "图标", 39 | "type": "icon" 40 | }, 41 | { 42 | "key": "app_id", 43 | "label": "AppID" 44 | }, 45 | { 46 | "key": "disabled", 47 | "label": "禁用", 48 | "type": "bool" 49 | }, 50 | { 51 | "key": "created", 52 | "label": "日期", 53 | "type": "date" 54 | } 55 | ], 56 | "load_api": "iot/project/:project_id/plugin/list?limit=99999", 57 | "load_success": "data.forEach(d=>{ d.icon = '/api/plugin/' + d.app_id + '/icon' })" 58 | } -------------------------------------------------------------------------------- /pages/project-user.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "项目用户", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "label": "绑定", 7 | "icon": "plus", 8 | "type": "button", 9 | "action": { 10 | "type": "dialog", 11 | "page": "user/choose", 12 | "after_close": "this.request.get('iot/project/'+this.params.project_id+'/user/'+result.id+'/bind').subscribe(res=>this.load())" 13 | } 14 | } 15 | ], 16 | "operators": [ 17 | { 18 | "icon": "delete", 19 | "title": "解绑", 20 | "confirm": "确认解绑?", 21 | "action": { 22 | "type": "script", 23 | "script": "this.request.get('iot/project/'+data.project_id+'/user/'+data.user_id+'/unbind').subscribe(res=>this.load())" 24 | } 25 | } 26 | ], 27 | "columns": [ 28 | { 29 | "key": "user_id", 30 | "label": "用户ID" 31 | }, 32 | { 33 | "key": "user", 34 | "label": "用户名" 35 | }, 36 | { 37 | "key": "admin", 38 | "label": "管理员", 39 | "type": "boolean" 40 | }, 41 | { 42 | "key": "created", 43 | "label": "日期", 44 | "type": "date" 45 | } 46 | ], 47 | "load_api": "iot/project/:project_id/user/list?limit=99999" 48 | } -------------------------------------------------------------------------------- /pages/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "项目", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "label": "创建", 7 | "icon": "plus", 8 | "type": "button", 9 | "action": { 10 | "type": "page", 11 | "page": "iot/project-create" 12 | } 13 | }, 14 | { 15 | "key": "keyword", 16 | "type": "text", 17 | "placeholder": "请输入关键字" 18 | }, 19 | { 20 | "type": "button", 21 | "icon": "search", 22 | "label": "搜索", 23 | "action": { 24 | "type": "script", 25 | "script": "this.keyword=this.toolbar.value.keyword; this.search()" 26 | } 27 | } 28 | ], 29 | "keywords": [ 30 | "id", 31 | "name", 32 | "description" 33 | ], 34 | "operators": [ 35 | { 36 | "icon": "eye", 37 | "action": { 38 | "type": "page", 39 | "page": "iot/project-detail", 40 | "params_func": "return {id: data.id}" 41 | } 42 | }, 43 | { 44 | "icon": "edit", 45 | "action": { 46 | "type": "page", 47 | "page": "iot/project-edit", 48 | "params_func": "return {id: data.id}" 49 | } 50 | }, 51 | { 52 | "icon": "delete", 53 | "title": "删除", 54 | "confirm": "确认删除?", 55 | "action": { 56 | "type": "script", 57 | "script": "this.request.get('iot/project/'+data.id+'/delete').subscribe(res=>{this.load()})" 58 | } 59 | } 60 | ], 61 | "columns": [ 62 | { 63 | "key": "id", 64 | "label": "ID", 65 | "action": { 66 | "type": "page", 67 | "page": "iot/project-detail", 68 | "params_func": "return {id: data.id}" 69 | } 70 | }, 71 | { 72 | "key": "name", 73 | "label": "名称" 74 | }, 75 | { 76 | "key": "description", 77 | "label": "说明" 78 | }, 79 | { 80 | "key": "disabled", 81 | "label": "禁用", 82 | "type": "boolean" 83 | }, 84 | { 85 | "key": "created", 86 | "label": "日期", 87 | "type": "date" 88 | } 89 | ], 90 | "search_api": "iot/project/search" 91 | } -------------------------------------------------------------------------------- /pages/protocol.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "协议库", 3 | "template": "table", 4 | "columns": [ 5 | { 6 | "key": "name", 7 | "label": "名称" 8 | }, 9 | { 10 | "key": "description", 11 | "label": "说明" 12 | }, 13 | { 14 | "key": "version", 15 | "label": "版本" 16 | }, 17 | { 18 | "key": "author", 19 | "label": "作者" 20 | }, 21 | { 22 | "key": "copyright", 23 | "label": "版权" 24 | } 25 | ], 26 | "load_api": "iot/protocol/list" 27 | } -------------------------------------------------------------------------------- /pages/space-choose.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "空间", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "key": "keyword", 7 | "type": "text", 8 | "placeholder": "请输入关键字" 9 | }, 10 | { 11 | "type": "button", 12 | "icon": "search", 13 | "label": "搜索", 14 | "action": { 15 | "type": "script", 16 | "script": "this.keyword=this.toolbar.value.keyword; this.search()" 17 | } 18 | } 19 | ], 20 | "keywords": [ 21 | "id", 22 | "name", 23 | "description" 24 | ], 25 | "operators": [ 26 | { 27 | "icon": "check", 28 | "label": "选择", 29 | "action": { 30 | "type": "script", 31 | "script": "this.modelRef.close(data)" 32 | } 33 | } 34 | ], 35 | "columns": [ 36 | { 37 | "key": "id", 38 | "label": "ID" 39 | }, 40 | { 41 | "key": "name", 42 | "label": "名称" 43 | }, 44 | { 45 | "key": "description", 46 | "label": "说明" 47 | } 48 | ], 49 | "search_api": "iot/space/search" 50 | } -------------------------------------------------------------------------------- /pages/space-create.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "创建空间", 3 | "template": "form", 4 | "toolbar": [ 5 | { 6 | "type": "button", 7 | "label": "选择项目ID", 8 | "action": { 9 | "type": "dialog", 10 | "page": "iot/project-choose", 11 | "after_close": "this.editor.patchValue({project_id: result.id})" 12 | } 13 | }, 14 | { 15 | "type": "button", 16 | "label": "选择父空间ID", 17 | "action": { 18 | "type": "dialog", 19 | "page": "iot/space-choose", 20 | "after_close": "this.editor.patchValue({parent_id: result.id})" 21 | } 22 | } 23 | ], 24 | "fields": [ 25 | { 26 | "key": "id", 27 | "label": "ID", 28 | "type": "text", 29 | "placeholder": "默认随机ID" 30 | }, 31 | { 32 | "key": "name", 33 | "label": "名称", 34 | "type": "text" 35 | }, 36 | { 37 | "key": "description", 38 | "label": "说明", 39 | "type": "text" 40 | }, 41 | { 42 | "key": "project_id", 43 | "label": "项目ID", 44 | "type": "text" 45 | }, 46 | { 47 | "key": "parent_id", 48 | "label": "父空间ID", 49 | "type": "text" 50 | }, 51 | { 52 | "key": "disabled", 53 | "label": "禁用", 54 | "type": "switch" 55 | } 56 | ], 57 | "submit_api": "iot/space/create", 58 | "submit_success": "this.navigate('/page/iot/space-detail?id='+data.id)", 59 | "mount": "", 60 | "methods": { 61 | } 62 | } -------------------------------------------------------------------------------- /pages/space-detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "空间详情", 3 | "template": "info", 4 | "toolbar": [ 5 | { 6 | "icon": "edit", 7 | "type": "button", 8 | "label": "编辑", 9 | "action": { 10 | "type": "page", 11 | "page": "iot/space-edit", 12 | "params_func": "return {id: data.id}" 13 | } 14 | }, 15 | { 16 | "icon": "delete", 17 | "type": "button", 18 | "label": "删除", 19 | "confirm": "确认删除?", 20 | "action": { 21 | "type": "script", 22 | "script": "this.request.get('iot/space/'+data.id+'/delete').subscribe(res=>{this.navigate('/page/iot/project?id='+data.project_id)})" 23 | } 24 | } 25 | ], 26 | "items": [ 27 | { 28 | "key": "id", 29 | "label": "ID" 30 | }, 31 | { 32 | "key": "name", 33 | "label": "名称" 34 | }, 35 | { 36 | "key": "description", 37 | "label": "说明" 38 | }, 39 | { 40 | "key": "project_id", 41 | "label": "项目ID", 42 | "action": { 43 | "type": "page", 44 | "page": "iot/project-detail", 45 | "params_func": "return {id: data.project_id}" 46 | } 47 | }, 48 | { 49 | "key": "parent_id", 50 | "label": "父空间ID", 51 | "action": { 52 | "type": "page", 53 | "page": "iot/space-detail", 54 | "params_func": "return {id: data.parent_id}" 55 | } 56 | }, 57 | { 58 | "key": "disabled", 59 | "label": "禁用", 60 | "type": "boolean" 61 | } 62 | ], 63 | "load_api": "iot/space/:id", 64 | "tabs": [ 65 | { 66 | "title": "空间设备", 67 | "page": "iot/space-device", 68 | "params_func": "return {space_id: params.id}" 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /pages/space-device.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "空间设备", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "label": "绑定", 7 | "icon": "plus", 8 | "type": "button", 9 | "action": { 10 | "type": "dialog", 11 | "page": "iot/project-device-choose", 12 | "params_func": "return {project_id: this.space.project_id}", 13 | "after_close": "this.request.get('iot/space/'+this.params.space_id+'/device/'+result.device_id+'/bind').subscribe(res=>this.load())" 14 | } 15 | } 16 | ], 17 | "operators": [ 18 | { 19 | "icon": "eye", 20 | "action": { 21 | "type": "page", 22 | "page": "iot/device-detail", 23 | "params_func": "return {id: data.id}" 24 | } 25 | }, 26 | { 27 | "icon": "edit", 28 | "action": { 29 | "type": "page", 30 | "page": "iot/device-edit", 31 | "params_func": "return {id: data.id}" 32 | } 33 | }, 34 | { 35 | "icon": "delete", 36 | "title": "解绑", 37 | "confirm": "确认解绑?", 38 | "action": { 39 | "type": "script", 40 | "script": "this.request.get('iot/space/'+data.space_id+'/device/'+data.device_id+'/unbind').subscribe(res=>this.load())" 41 | } 42 | } 43 | ], 44 | "columns": [ 45 | { 46 | "key": "device_id", 47 | "label": "设备ID", 48 | "action": { 49 | "type": "page", 50 | "page": "iot/device-detail", 51 | "params_func": "return {id: data.device_id}" 52 | } 53 | }, 54 | { 55 | "key": "name", 56 | "label": "名称" 57 | }, 58 | { 59 | "key": "created", 60 | "label": "日期", 61 | "type": "date" 62 | } 63 | ], 64 | "load_api": "iot/space/:space_id/device/list?limit=99999", 65 | "mount": "this.request.get('iot/space/'+this.params.space_id).subscribe(res=>{this.space=res.data})" 66 | } -------------------------------------------------------------------------------- /pages/space-edit.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "创建空间", 3 | "template": "form", 4 | "toolbar": [ 5 | { 6 | "type": "button", 7 | "label": "选择项目ID", 8 | "action": { 9 | "type": "dialog", 10 | "page": "iot/project-choose", 11 | "after_close": "this.editor.patchValue({project_id: result.id})" 12 | } 13 | }, 14 | { 15 | "type": "button", 16 | "label": "选择父空间ID", 17 | "action": { 18 | "type": "dialog", 19 | "page": "iot/space-choose", 20 | "after_close": "this.editor.patchValue({parent_id: result.id})" 21 | } 22 | } 23 | ], 24 | "fields": [ 25 | { 26 | "key": "id", 27 | "label": "ID", 28 | "type": "text", 29 | "placeholder": "默认随机ID" 30 | }, 31 | { 32 | "key": "name", 33 | "label": "名称", 34 | "type": "text" 35 | }, 36 | { 37 | "key": "description", 38 | "label": "说明", 39 | "type": "text" 40 | }, 41 | { 42 | "key": "project_id", 43 | "label": "项目ID", 44 | "type": "text" 45 | }, 46 | { 47 | "key": "parent_id", 48 | "label": "父空间ID", 49 | "type": "text" 50 | }, 51 | { 52 | "key": "disabled", 53 | "label": "禁用", 54 | "type": "switch" 55 | } 56 | ], 57 | "load_api": "iot/space/:id", 58 | "submit_api": "iot/space/:id", 59 | "submit_success": "this.navigate('/page/iot/space-detail?id='+data.id)", 60 | "mount": "", 61 | "methods": { 62 | } 63 | } -------------------------------------------------------------------------------- /pages/space.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "空间", 3 | "template": "table", 4 | "toolbar": [ 5 | { 6 | "label": "创建", 7 | "icon": "plus", 8 | "type": "button", 9 | "action": { 10 | "type": "page", 11 | "page": "iot/space-create" 12 | } 13 | } 14 | ], 15 | "operators": [ 16 | { 17 | "icon": "eye", 18 | "action": { 19 | "type": "page", 20 | "page": "iot/space-detail", 21 | "params_func": "return {id: data.id}" 22 | } 23 | }, 24 | { 25 | "icon": "edit", 26 | "action": { 27 | "type": "page", 28 | "page": "iot/space-edit", 29 | "params_func": "return {id: data.id}" 30 | } 31 | }, 32 | { 33 | "icon": "delete", 34 | "title": "删除", 35 | "confirm": "确认删除?", 36 | "action": { 37 | "type": "script", 38 | "script": "this.request.get('iot/space/'+data.id+'/delete').subscribe(res=>{this.load()})" 39 | } 40 | } 41 | ], 42 | "columns": [ 43 | { 44 | "key": "id", 45 | "label": "ID", 46 | "action": { 47 | "type": "page", 48 | "page": "iot/space-detail", 49 | "params_func": "return {id: data.id}" 50 | } 51 | }, 52 | { 53 | "key": "parent_id", 54 | "label": "父空间ID", 55 | "action": { 56 | "type": "page", 57 | "page": "iot/space-detail", 58 | "params_func": "return {id: data.parent_id}" 59 | } 60 | }, 61 | { 62 | "key": "name", 63 | "label": "名称" 64 | }, 65 | { 66 | "key": "description", 67 | "label": "说明" 68 | }, 69 | { 70 | "key": "disabled", 71 | "label": "禁用", 72 | "type": "boolean" 73 | }, 74 | { 75 | "key": "created", 76 | "label": "日期", 77 | "type": "date" 78 | } 79 | ], 80 | "search_api": "iot/space/search", 81 | "mount": "if(this.params.project_id)this.filter.project_id=this.params.project_id; if(this.params.parent_id)this.filter.parent_id=this.params.parent_id" 82 | } -------------------------------------------------------------------------------- /product/config.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/busy-cloud/boat/db" 7 | "github.com/busy-cloud/boat/lib" 8 | "strings" 9 | "xorm.io/xorm/schemas" 10 | ) 11 | 12 | var configCache = lib.CacheLoader[ProductConfig]{ 13 | Timeout: 600, 14 | Loader: func(key string) (*ProductConfig, error) { 15 | var cfg ProductConfig 16 | ss := strings.Split(key, "/") 17 | has, err := db.Engine().ID(schemas.PK{ss[0], ss[1]}).Get(&cfg) 18 | if err != nil { 19 | return nil, err 20 | } 21 | if !has { 22 | return nil, fmt.Errorf("empty product config %s", key) 23 | } 24 | return &cfg, nil 25 | }, 26 | } 27 | 28 | func LoadConfig[T any](id, config string) (*T, error) { 29 | idd := id + "/" + config 30 | 31 | c, err := configCache.Load(idd) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | //这里转来转去 37 | buf, err := json.Marshal(c.Content) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | var t T 43 | err = json.Unmarshal(buf, &t) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return &t, nil 49 | } 50 | -------------------------------------------------------------------------------- /product/model.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "fmt" 5 | "github.com/busy-cloud/boat/db" 6 | "github.com/busy-cloud/boat/lib" 7 | "time" 8 | ) 9 | 10 | // Property 属性 11 | type Property struct { 12 | Name string `json:"name,omitempty"` //变量名称 13 | Label string `json:"label,omitempty"` //显示名称 14 | Unit string `json:"unit,omitempty"` //单位 15 | Type string `json:"type,omitempty"` //bool string number array object 16 | Precision uint8 `json:"precision,omitempty"` 17 | Default any `json:"default,omitempty"` //默认值 18 | Writable bool `json:"writable,omitempty"` //是否可写 19 | History bool `json:"history,omitempty"` //是否保存历史 20 | } 21 | 22 | type Parameter struct { 23 | Name string `json:"name,omitempty"` 24 | Type string `json:"type,omitempty"` 25 | } 26 | 27 | type Event struct { 28 | Name string `json:"name,omitempty"` 29 | Description string `json:"description,omitempty"` 30 | Parameters []Parameter `json:"parameters,omitempty"` 31 | } 32 | 33 | type Action struct { 34 | Name string `json:"name,omitempty"` 35 | Description string `json:"description,omitempty"` 36 | Parameters []Parameter `json:"parameters,omitempty"` 37 | Returns []Parameter `json:"returns,omitempty"` 38 | } 39 | 40 | type ProductModel struct { 41 | Id string `json:"id,omitempty" xorm:"pk"` 42 | Properties []*Property `json:"properties,omitempty" xorm:"json"` 43 | Events []*Event `json:"events,omitempty" xorm:"json"` 44 | Actions []*Action `json:"actions,omitempty" xorm:"json"` 45 | Validators []*Validator `json:"validators,omitempty" xorm:"json"` 46 | Updated time.Time `json:"updated,omitempty" xorm:"updated"` 47 | Created time.Time `json:"created,omitempty" xorm:"created"` 48 | } 49 | 50 | var modelCache = lib.CacheLoader[ProductModel]{ 51 | Timeout: 600, 52 | Loader: func(key string) (*ProductModel, error) { 53 | var pm ProductModel 54 | has, err := db.Engine().ID(key).Get(&pm) 55 | if err != nil { 56 | return nil, err 57 | } 58 | if !has { 59 | return nil, fmt.Errorf("empty product model %s", key) 60 | } 61 | 62 | return &pm, nil 63 | }, 64 | } 65 | 66 | func LoadModel(id string) (*ProductModel, error) { 67 | return modelCache.Load(id) 68 | } 69 | -------------------------------------------------------------------------------- /product/point.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/god-jason/iot-master/bin" 7 | "github.com/spf13/cast" 8 | ) 9 | 10 | type Point interface { 11 | Encode(data any) ([]byte, error) 12 | Parse(address uint16, buf []byte) (any, error) 13 | } 14 | 15 | type PointBit struct { 16 | Name string `json:"name"` //名称 17 | Address uint16 `json:"address"` //偏移 18 | } 19 | 20 | func (p *PointBit) Encode(data any) ([]byte, error) { 21 | val := cast.ToBool(data) 22 | if val { 23 | return []byte{0xFF, 00}, nil 24 | } else { 25 | return []byte{0x00, 00}, nil 26 | } 27 | } 28 | 29 | func (p *PointBit) Parse(address uint16, buf []byte) (any, error) { 30 | l := len(buf) 31 | offset := int(p.Address - address) 32 | cur := offset / 8 33 | bit := offset % 8 34 | 35 | if cur >= l { 36 | return nil, errors.New("长度不够") 37 | } 38 | 39 | ret := buf[cur] & (1 << bit) 40 | 41 | return ret > 0, nil 42 | } 43 | 44 | type PointWord struct { 45 | Name string `json:"name"` //名称 46 | Type string `json:"type"` //类型 47 | Address uint16 `json:"address"` //偏移 48 | BigEndian bool `json:"be,omitempty"` //大端模式 49 | Rate float64 `json:"rate,omitempty"` //倍率 50 | Correct float64 `json:"correct,omitempty"` //纠正 51 | Bits []*Bit `json:"bits,omitempty"` //位,1 2 3... 52 | } 53 | 54 | type Bit struct { 55 | Name string `json:"name"` //名称 56 | Bit int `json:"bit"` //偏移 57 | } 58 | 59 | func (p *PointWord) Encode(data any) ([]byte, error) { 60 | var ret []byte 61 | 62 | //纠正 63 | if p.Correct != 0 { 64 | data = cast.ToFloat64(data) - p.Correct 65 | } 66 | 67 | //倍率逆转换 68 | if p.Rate != 0 && p.Rate != 1 { 69 | data = cast.ToFloat64(data) / p.Rate 70 | } 71 | 72 | switch p.Type { 73 | case "short", "int16": 74 | ret = make([]byte, 2) 75 | val := cast.ToInt16(data) 76 | if p.BigEndian { 77 | bin.WriteUint16(ret, uint16(val)) 78 | } else { 79 | bin.WriteUint16LittleEndian(ret, uint16(val)) 80 | } 81 | case "word", "uint16": 82 | ret = make([]byte, 2) 83 | val := cast.ToUint16(data) 84 | if p.BigEndian { 85 | bin.WriteUint16(ret, val) 86 | } else { 87 | bin.WriteUint16LittleEndian(ret, val) 88 | } 89 | case "int32", "int": 90 | ret = make([]byte, 4) 91 | val := cast.ToInt32(data) 92 | if p.BigEndian { 93 | bin.WriteUint32(ret, uint32(val)) 94 | } else { 95 | bin.WriteUint32LittleEndian(ret, uint32(val)) 96 | } 97 | case "qword", "uint32", "uint": 98 | ret = make([]byte, 4) 99 | val := cast.ToUint32(data) 100 | if p.BigEndian { 101 | bin.WriteUint32(ret, val) 102 | } else { 103 | bin.WriteUint32LittleEndian(ret, val) 104 | } 105 | case "float", "float32": 106 | ret = make([]byte, 4) 107 | val := cast.ToFloat32(data) 108 | if p.BigEndian { 109 | bin.WriteFloat32(ret, val) 110 | } else { 111 | bin.WriteFloat32LittleEndian(ret, val) 112 | } 113 | case "double", "float64": 114 | ret = make([]byte, 8) 115 | val := cast.ToFloat64(data) 116 | if p.BigEndian { 117 | bin.WriteFloat64(ret, val) 118 | } else { 119 | bin.WriteFloat64LittleEndian(ret, val) 120 | } 121 | } 122 | 123 | return ret, nil 124 | } 125 | 126 | func (p *PointWord) Parse(address uint16, buf []byte) (any, error) { 127 | l := len(buf) 128 | 129 | offset := int((p.Address - address) * 2) 130 | //offset := p.Offset << 1 131 | if offset >= l { 132 | return nil, errors.New("长度不够") 133 | } 134 | 135 | var ret any 136 | switch p.Type { 137 | case "short", "int16": 138 | if len(buf[offset:]) < 2 { 139 | return nil, fmt.Errorf("int16长度不足2:%d", l) 140 | } 141 | if p.BigEndian { 142 | ret = int16(bin.ParseUint16(buf[offset:])) 143 | } else { 144 | ret = int16(bin.ParseUint16LittleEndian(buf[offset:])) 145 | } 146 | case "word", "uint16": 147 | if len(buf[offset:]) < 2 { 148 | return nil, fmt.Errorf("uint16长度不足2:%d", l) 149 | } 150 | if p.BigEndian { 151 | ret = bin.ParseUint16(buf[offset:]) 152 | } else { 153 | ret = bin.ParseUint16LittleEndian(buf[offset:]) 154 | } 155 | //取位 156 | if p.Bits != nil && len(p.Bits) > 0 { 157 | rets := make(map[string]bool) 158 | for _, b := range p.Bits { 159 | rets[b.Name] = (ret.(uint16))&(1< 0 160 | } 161 | return rets, nil 162 | } 163 | case "int32", "int": 164 | if len(buf[offset:]) < 4 { 165 | return nil, fmt.Errorf("int32长度不足4:%d", l) 166 | } 167 | if p.BigEndian { 168 | ret = int32(bin.ParseUint32(buf[offset:])) 169 | } else { 170 | ret = int32(bin.ParseUint32LittleEndian(buf[offset:])) 171 | } 172 | case "qword", "uint32", "uint": 173 | if len(buf[offset:]) < 4 { 174 | return nil, fmt.Errorf("uint32长度不足4:%d", l) 175 | } 176 | if p.BigEndian { 177 | ret = bin.ParseUint32(buf[offset:]) 178 | } else { 179 | ret = bin.ParseUint32LittleEndian(buf[offset:]) 180 | } 181 | case "float", "float32": 182 | if len(buf[offset:]) < 4 { 183 | return nil, fmt.Errorf("float32长度不足4:%d", l) 184 | } 185 | if p.BigEndian { 186 | ret = bin.ParseFloat32(buf[offset:]) 187 | } else { 188 | ret = bin.ParseFloat32LittleEndian(buf[offset:]) 189 | } 190 | case "double", "float64": 191 | if len(buf[offset:]) < 4 { 192 | return nil, fmt.Errorf("float64长度不足8:%d", l) 193 | } 194 | if p.BigEndian { 195 | ret = bin.ParseFloat64(buf[offset:]) 196 | } else { 197 | ret = bin.ParseFloat64LittleEndian(buf[offset:]) 198 | } 199 | default: 200 | return nil, fmt.Errorf("不支持的数据类型 %s", p.Type) 201 | } 202 | 203 | //倍率 204 | if p.Rate != 0 && p.Rate != 1 { 205 | ret = cast.ToFloat64(ret) * p.Rate 206 | } 207 | 208 | //校准 209 | if p.Correct != 0 { 210 | ret = cast.ToFloat64(ret) + p.Correct 211 | } 212 | 213 | return ret, nil 214 | } 215 | 216 | func (p *PointWord) Size() int { 217 | switch p.Type { 218 | case "short", "int16": 219 | return 1 220 | case "word", "uint16": 221 | return 1 222 | case "int32", "int": 223 | return 2 224 | case "qword", "uint32", "uint": 225 | return 2 226 | case "float", "float32": 227 | return 2 228 | case "double", "float64": 229 | return 4 230 | default: 231 | return 1 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /product/product.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/db" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | db.Register(&Product{}, &ProductConfig{}, &ProductModel{}) 10 | } 11 | 12 | type Product struct { 13 | Id string `json:"id,omitempty" xorm:"pk"` 14 | Name string `json:"name,omitempty"` 15 | Description string `json:"description,omitempty"` 16 | Type string `json:"type,omitempty"` //类型 17 | Version string `json:"version,omitempty"` 18 | Protocol string `json:"protocol,omitempty"` 19 | Disabled bool `json:"disabled,omitempty"` //禁用 20 | Created time.Time `json:"created,omitempty" xorm:"created"` 21 | } 22 | 23 | type ProductConfig struct { 24 | Id string `json:"id" xorm:"pk"` 25 | Name string `json:"name" xorm:"pk"` //双主键 26 | Content map[string]any `json:"content,omitempty" xorm:"json text"` 27 | Updated time.Time `json:"updated,omitempty" xorm:"updated"` 28 | Created time.Time `json:"created,omitempty" xorm:"created"` 29 | } 30 | -------------------------------------------------------------------------------- /product/product_api.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/api" 5 | "github.com/busy-cloud/boat/curd" 6 | "github.com/busy-cloud/boat/db" 7 | "github.com/gin-gonic/gin" 8 | "xorm.io/xorm/schemas" 9 | ) 10 | 11 | func init() { 12 | api.Register("GET", "iot/product/list", curd.ApiList[Product]()) 13 | api.Register("POST", "iot/product/search", curd.ApiSearch[Product]()) 14 | api.Register("POST", "iot/product/create", curd.ApiCreate[Product]()) 15 | api.Register("GET", "iot/product/:id", curd.ApiGet[Product]()) 16 | api.Register("POST", "iot/product/:id", curd.ApiUpdate[Product]("id", "name", "description", "type", "version", "protocol", "disabled")) 17 | api.Register("GET", "iot/product/:id/delete", curd.ApiDelete[Product]()) 18 | api.Register("GET", "iot/product/:id/enable", curd.ApiDisable[Product](false)) 19 | api.Register("GET", "iot/product/:id/disable", curd.ApiDisable[Product](true)) 20 | 21 | //物模型 22 | api.Register("GET", "iot/product/:id/model", curd.ApiGet[ProductModel]()) 23 | api.Register("POST", "iot/product/:id/model", productModelUpdate) 24 | 25 | //配置接口,一般用于协议点表等 26 | api.Register("GET", "iot/product/:id/config/:name", productConfig) 27 | api.Register("POST", "iot/product/:id/config/:name", productConfigUpdate) 28 | } 29 | 30 | func productModelUpdate(ctx *gin.Context) { 31 | id := ctx.Param("id") 32 | 33 | var model ProductModel 34 | err := ctx.ShouldBind(&model) 35 | if err != nil { 36 | api.Error(ctx, err) 37 | return 38 | } 39 | model.Id = id 40 | 41 | _, err = db.Engine().ID(id).Delete(new(ProductModel)) //不管有没有都删掉 42 | _, err = db.Engine().ID(id).Insert(&model) 43 | if err != nil { 44 | api.Error(ctx, err) 45 | return 46 | } 47 | 48 | api.OK(ctx, &model) 49 | } 50 | 51 | func productConfig(ctx *gin.Context) { 52 | var config ProductConfig 53 | has, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("name")}).Get(&config) 54 | if err != nil { 55 | api.Error(ctx, err) 56 | return 57 | } 58 | if !has { 59 | api.Fail(ctx, "找不到配置文件") 60 | return 61 | } 62 | 63 | api.OK(ctx, config.Content) 64 | } 65 | 66 | func productConfigUpdate(ctx *gin.Context) { 67 | //body, err := io.ReadAll(ctx.Request.Body) 68 | //if err != nil { 69 | // api.Error(ctx, err) 70 | // return 71 | //} 72 | // 73 | var body map[string]any 74 | err := ctx.ShouldBind(&body) 75 | if err != nil { 76 | api.Error(ctx, err) 77 | return 78 | } 79 | 80 | config := ProductConfig{ 81 | Id: ctx.Param("id"), 82 | Name: ctx.Param("name"), 83 | Content: body, 84 | } 85 | 86 | _, err = db.Engine().ID(schemas.PK{config.Id, config.Name}).Delete(new(ProductConfig)) 87 | _, err = db.Engine().ID(schemas.PK{config.Id, config.Name}).Insert(&config) 88 | if err != nil { 89 | api.Error(ctx, err) 90 | return 91 | } 92 | 93 | api.OK(ctx, &config) 94 | } 95 | -------------------------------------------------------------------------------- /product/validator.go: -------------------------------------------------------------------------------- 1 | package product 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cast" 6 | ) 7 | 8 | type Compare struct { 9 | Type string `json:"type"` //= != > >= < <= 10 | Name string `json:"name"` 11 | Value float64 `json:"value"` 12 | } 13 | 14 | func (c *Compare) Evaluate(ctx map[string]any) (bool, error) { 15 | val, ok := ctx[c.Name] 16 | if !ok { 17 | return false, fmt.Errorf("compare evalute field %s not found", c.Name) 18 | } 19 | v, err := cast.ToFloat64E(val) 20 | if err != nil { 21 | return false, err 22 | } 23 | switch c.Type { 24 | case "=", "==": 25 | return v == c.Value, nil 26 | case "!=", "~=", "<>": 27 | return v != c.Value, nil 28 | case ">": 29 | return v > c.Value, nil 30 | case "<": 31 | return v < c.Value, nil 32 | case ">=": 33 | return v >= c.Value, nil 34 | case "<=": 35 | return v <= c.Value, nil 36 | default: 37 | return false, fmt.Errorf("unsupported compare type: %s", c.Type) 38 | } 39 | } 40 | 41 | type Validator struct { 42 | Type string `json:"type"` //compare对比, expression表达式 43 | Compare Compare `json:"compare,omitempty"` 44 | Expression string `json:"expression,omitempty"` 45 | Title string `json:"title,omitempty"` 46 | Message string `json:"message,omitempty"` 47 | Level int `json:"level,omitempty"` 48 | Delay int64 `json:"delay,omitempty"` 49 | Reset int64 `json:"reset,omitempty"` 50 | ResetTimes int `json:"reset_times,omitempty"` 51 | Disabled bool `json:"disabled,omitempty"` 52 | } 53 | -------------------------------------------------------------------------------- /project/app-api.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/api" 5 | "github.com/busy-cloud/boat/db" 6 | "github.com/gin-gonic/gin" 7 | "xorm.io/xorm/schemas" 8 | ) 9 | 10 | func init() { 11 | api.Register("GET", "iot/project/:id/app/list", projectAppList) 12 | api.Register("GET", "iot/project/:id/app/:app/exists", projectAppExists) 13 | api.Register("GET", "iot/project/:id/app/:app/bind", projectAppBind) 14 | api.Register("GET", "iot/project/:id/app/:app/unbind", projectAppUnbind) 15 | api.Register("GET", "iot/project/:id/app/:app/disable", projectAppDisable) 16 | api.Register("GET", "iot/project/:id/app/:app/enable", projectAppEnable) 17 | } 18 | 19 | // @Summary 项目应用列表 20 | // @Schemes 21 | // @Description 项目应用列表 22 | // @Tags project-app 23 | // @Param id path int true "项目ID" 24 | // @Accept json 25 | // @Produce json 26 | // @Success 200 {object} curd.ReplyData[[]ProjectApp] 返回项目应用信息 27 | // @Router iot/project/{id}/app/list [get] 28 | func projectAppList(ctx *gin.Context) { 29 | var pds []ProjectApp 30 | err := db.Engine().Where("project_id=?", ctx.Param("id")).Find(&pds) 31 | if err != nil { 32 | api.Error(ctx, err) 33 | return 34 | } 35 | api.OK(ctx, pds) 36 | } 37 | 38 | // @Summary 判断项目应用是否存在 39 | // @Schemes 40 | // @Description 判断项目应用是否存在 41 | // @Tags project-app 42 | // @Param id path int true "项目ID" 43 | // @Param app path int true "应用ID" 44 | // @Accept json 45 | // @Produce json 46 | // @Success 200 {object} curd.ReplyData[bool] 47 | // @Router iot/project/{id}/app/{app}/exists [get] 48 | func projectAppExists(ctx *gin.Context) { 49 | pd := ProjectApp{ 50 | ProjectId: ctx.Param("id"), 51 | AppId: ctx.Param("app"), 52 | } 53 | has, err := db.Engine().Exist(&pd) 54 | if err != nil { 55 | api.Error(ctx, err) 56 | return 57 | } 58 | api.OK(ctx, has) 59 | } 60 | 61 | // @Summary 绑定项目应用 62 | // @Schemes 63 | // @Description 绑定项目应用 64 | // @Tags project-app 65 | // @Param id path int true "项目ID" 66 | // @Param app path int true "应用ID" 67 | // @Accept json 68 | // @Produce json 69 | // @Success 200 {object} curd.ReplyData[int] 70 | // @Router iot/project/{id}/app/{app}/bind [get] 71 | func projectAppBind(ctx *gin.Context) { 72 | pd := ProjectApp{ 73 | ProjectId: ctx.Param("id"), 74 | AppId: ctx.Param("app"), 75 | } 76 | _, err := db.Engine().InsertOne(&pd) 77 | if err != nil { 78 | api.Error(ctx, err) 79 | return 80 | } 81 | api.OK(ctx, nil) 82 | } 83 | 84 | // @Summary 删除项目应用 85 | // @Schemes 86 | // @Description 删除项目应用 87 | // @Tags project-app 88 | // @Param id path int true "项目ID" 89 | // @Param app path int true "应用ID" 90 | // @Accept json 91 | // @Produce json 92 | // @Success 200 {object} curd.ReplyData[int] 93 | // @Router iot/project/{id}/app/{app}/unbind [get] 94 | func projectAppUnbind(ctx *gin.Context) { 95 | _, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("app")}).Delete(new(ProjectApp)) 96 | if err != nil { 97 | api.Error(ctx, err) 98 | return 99 | } 100 | api.OK(ctx, nil) 101 | } 102 | 103 | // @Summary 禁用项目应用 104 | // @Schemes 105 | // @Description 禁用项目应用 106 | // @Tags project-app 107 | // @Param id path int true "项目ID" 108 | // @Param app path int true "应用ID" 109 | // @Accept json 110 | // @Produce json 111 | // @Success 200 {object} curd.ReplyData[int] 112 | // @Router iot/project/{id}/app/{app}/disable [get] 113 | func projectAppDisable(ctx *gin.Context) { 114 | pd := ProjectApp{Disabled: true} 115 | _, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("app")}).Cols("disabled").Update(&pd) 116 | if err != nil { 117 | api.Error(ctx, err) 118 | return 119 | } 120 | api.OK(ctx, nil) 121 | } 122 | 123 | // @Summary 启用项目应用 124 | // @Schemes 125 | // @Description 启用项目应用 126 | // @Tags project-app 127 | // @Param id path int true "项目ID" 128 | // @Param app path int true "应用ID" 129 | // @Accept json 130 | // @Produce json 131 | // @Success 200 {object} curd.ReplyData[int] 132 | // @Router iot/project/{id}/app/{app}/enable [get] 133 | func projectAppEnable(ctx *gin.Context) { 134 | pd := ProjectApp{Disabled: false} 135 | _, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("app")}).Cols("disabled").Update(&pd) 136 | if err != nil { 137 | api.Error(ctx, err) 138 | return 139 | } 140 | api.OK(ctx, nil) 141 | } 142 | -------------------------------------------------------------------------------- /project/device-api.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/api" 5 | "github.com/busy-cloud/boat/db" 6 | "github.com/gin-gonic/gin" 7 | "xorm.io/xorm/schemas" 8 | ) 9 | 10 | func init() { 11 | api.Register("GET", "iot/project/:id/device/list", projectDeviceList) 12 | api.Register("GET", "iot/project/:id/device/:device/bind", projectDeviceBind) 13 | api.Register("GET", "iot/project/:id/device/:device/unbind", projectDeviceUnbind) 14 | api.Register("POST", "iot/project/:id/device/:device", projectDeviceUpdate) 15 | } 16 | 17 | // @Summary 空间设备列表 18 | // @Schemes 19 | // @Description 空间设备列表 20 | // @Tags project-device 21 | // @Param id path int true "项目ID" 22 | // @Accept json 23 | // @Produce json 24 | // @Success 200 {object} curd.ReplyData[[]ProjectDevice] 返回空间设备信息 25 | // @Router iot/project/{id}/device/list [get] 26 | func projectDeviceList(ctx *gin.Context) { 27 | var pds []ProjectDevice 28 | err := db.Engine(). 29 | Select("project_device.project_id, project_device.device_id, project_device.name, project_device.created, device.name as device"). 30 | Join("INNER", "device", "device.id=project_device.device_id"). 31 | Where("project_device.project_id=?", ctx.Param("id")). 32 | Find(&pds) 33 | if err != nil { 34 | api.Error(ctx, err) 35 | return 36 | } 37 | api.OK(ctx, pds) 38 | } 39 | 40 | // @Summary 绑定空间设备 41 | // @Schemes 42 | // @Description 绑定空间设备 43 | // @Tags project-device 44 | // @Param id path int true "项目ID" 45 | // @Param device path int true "设备ID" 46 | // @Accept json 47 | // @Produce json 48 | // @Success 200 {object} curd.ReplyData[int] 49 | // @Router iot/project/{id}/device/{device}/bind [get] 50 | func projectDeviceBind(ctx *gin.Context) { 51 | pd := ProjectDevice{ 52 | ProjectId: ctx.Param("id"), 53 | DeviceId: ctx.Param("device"), 54 | } 55 | _, err := db.Engine().InsertOne(&pd) 56 | if err != nil { 57 | api.Error(ctx, err) 58 | return 59 | } 60 | api.OK(ctx, nil) 61 | } 62 | 63 | // @Summary 删除空间设备 64 | // @Schemes 65 | // @Description 删除空间设备 66 | // @Tags project-device 67 | // @Param id path int true "项目ID" 68 | // @Param device path int true "设备ID" 69 | // @Accept json 70 | // @Produce json 71 | // @Success 200 {object} curd.ReplyData[int] 72 | // @Router iot/project/{id}/device/{device}/unbind [get] 73 | func projectDeviceUnbind(ctx *gin.Context) { 74 | _, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("device")}).Delete(new(ProjectDevice)) 75 | if err != nil { 76 | api.Error(ctx, err) 77 | return 78 | } 79 | api.OK(ctx, nil) 80 | } 81 | 82 | // @Summary 修改空间设备 83 | // @Schemes 84 | // @Description 修改空间设备 85 | // @Tags project-device 86 | // @Param id path int true "项目ID" 87 | // @Param device path int true "设备ID" 88 | // @Param project-device body ProjectDevice true "空间设备信息" 89 | // @Accept json 90 | // @Produce json 91 | // @Success 200 {object} curd.ReplyData[int] 92 | // @Router iot/project/{id}/device/{device} [post] 93 | func projectDeviceUpdate(ctx *gin.Context) { 94 | var pd ProjectDevice 95 | err := ctx.ShouldBindJSON(&pd) 96 | if err != nil { 97 | api.Error(ctx, err) 98 | return 99 | } 100 | _, err = db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("device")}). 101 | Cols("device_id", "name", "disabled"). 102 | Update(&pd) 103 | if err != nil { 104 | api.Error(ctx, err) 105 | return 106 | } 107 | api.OK(ctx, nil) 108 | } 109 | -------------------------------------------------------------------------------- /project/project-api.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/api" 5 | "github.com/busy-cloud/boat/curd" 6 | ) 7 | 8 | func init() { 9 | 10 | api.Register("POST", "iot/project/count", curd.ApiCount[Project]()) 11 | api.Register("POST", "iot/project/search", curd.ApiSearch[Project]()) 12 | api.Register("GET", "iot/project/list", curd.ApiList[Project]()) 13 | api.Register("POST", "iot/project/create", curd.ApiCreate[Project]()) 14 | api.Register("GET", "iot/project/:id", curd.ApiGet[Project]()) 15 | api.Register("POST", "iot/project/:id", curd.ApiUpdate[Project]()) 16 | api.Register("GET", "iot/project/:id/delete", curd.ApiDelete[Project]()) 17 | api.Register("GET", "iot/project/:id/disable", curd.ApiDisable[Project](true)) 18 | api.Register("GET", "iot/project/:id/enable", curd.ApiDisable[Project](false)) 19 | } 20 | 21 | // @Summary 查询项目 22 | // @Schemes 23 | // @Description 查询项目 24 | // @Tags project 25 | // @Param search body curd.ParamSearch true "查询参数" 26 | // @Accept json 27 | // @Produce json 28 | // @Success 200 {object} curd.ReplyList[Project] 返回项目信息 29 | // @Router iot/project/search [post] 30 | func noopProjectSearch() {} 31 | 32 | // @Summary 查询项目 33 | // @Schemes 34 | // @Description 查询项目 35 | // @Tags project 36 | // @Param search query curd.ParamList true "查询参数" 37 | // @Accept json 38 | // @Produce json 39 | // @Success 200 {object} curd.ReplyList[Project] 返回项目信息 40 | // @Router iot/project/list [get] 41 | func noopProjectList() {} 42 | 43 | // @Summary 创建项目 44 | // @Schemes 45 | // @Description 创建项目 46 | // @Tags project 47 | // @Param search body Project true "项目信息" 48 | // @Accept json 49 | // @Produce json 50 | // @Success 200 {object} curd.ReplyData[Project] 返回项目信息 51 | // @Router iot/project/create [post] 52 | func noopProjectCreate() {} 53 | 54 | // @Summary 修改项目 55 | // @Schemes 56 | // @Description 修改项目 57 | // @Tags project 58 | // @Param id path int true "项目ID" 59 | // @Param project body Project true "项目信息" 60 | // @Accept json 61 | // @Produce json 62 | // @Success 200 {object} curd.ReplyData[Project] 返回项目信息 63 | // @Router iot/project/{id} [post] 64 | func noopProjectUpdate() {} 65 | 66 | // @Summary 删除项目 67 | // @Schemes 68 | // @Description 删除项目 69 | // @Tags project 70 | // @Param id path int true "项目ID" 71 | // @Accept json 72 | // @Produce json 73 | // @Success 200 {object} curd.ReplyData[Project] 返回项目信息 74 | // @Router iot/project/{id}/delete [get] 75 | func noopProjectDelete() {} 76 | 77 | // @Summary 启用项目 78 | // @Schemes 79 | // @Description 启用项目 80 | // @Tags project 81 | // @Param id path int true "项目ID" 82 | // @Accept json 83 | // @Produce json 84 | // @Success 200 {object} curd.ReplyData[Project] 返回项目信息 85 | // @Router iot/project/{id}/enable [get] 86 | func noopProjectEnable() {} 87 | 88 | // @Summary 禁用项目 89 | // @Schemes 90 | // @Description 禁用项目 91 | // @Tags project 92 | // @Param id path int true "项目ID" 93 | // @Accept json 94 | // @Produce json 95 | // @Success 200 {object} curd.ReplyData[Project] 返回项目信息 96 | // @Router iot/project/{id}/disable [get] 97 | func noopProjectDisable() {} 98 | -------------------------------------------------------------------------------- /project/project.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/db" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | db.Register(new(Project), new(ProjectUser), new(ProjectDevice), new(ProjectApp)) 10 | } 11 | 12 | type Project struct { 13 | Id string `json:"id" xorm:"pk"` 14 | Name string `json:"name,omitempty"` //名称 15 | Description string `json:"description,omitempty"` //说明 16 | Keywords []string `json:"keywords,omitempty"` //关键字 17 | Disabled bool `json:"disabled,omitempty"` 18 | Created time.Time `json:"created" xorm:"created"` 19 | } 20 | 21 | type ProjectUser struct { 22 | ProjectId string `json:"project_id,omitempty" xorm:"pk"` 23 | Project string `json:"project,omitempty" xorm:"<-"` 24 | UserId string `json:"user_id,omitempty" xorm:"pk"` 25 | User string `json:"user,omitempty" xorm:"<-"` 26 | Admin bool `json:"admin,omitempty"` 27 | Disabled bool `json:"disabled,omitempty"` 28 | Created time.Time `json:"created" xorm:"created"` 29 | } 30 | 31 | type ProjectDevice struct { 32 | ProjectId string `json:"project_id,omitempty" xorm:"pk"` 33 | Project string `json:"project,omitempty" xorm:"<-"` 34 | DeviceId string `json:"device_id,omitempty" xorm:"pk"` 35 | Device string `json:"device,omitempty" xorm:"<-"` 36 | Name string `json:"name,omitempty"` //编程别名 37 | Created time.Time `json:"created" xorm:"created"` 38 | } 39 | 40 | type ProjectApp struct { 41 | ProjectId string `json:"project_id,omitempty" xorm:"pk"` 42 | AppId string `json:"app_id,omitempty" xorm:"pk"` 43 | Disabled bool `json:"disabled,omitempty"` 44 | Created time.Time `json:"created" xorm:"created"` 45 | } 46 | -------------------------------------------------------------------------------- /project/user-api.go: -------------------------------------------------------------------------------- 1 | package project 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/api" 5 | "github.com/busy-cloud/boat/db" 6 | "github.com/gin-gonic/gin" 7 | "xorm.io/xorm/schemas" 8 | ) 9 | 10 | func init() { 11 | api.Register("GET", "iot/project/:id/user/list", projectUserList) 12 | api.Register("GET", "iot/project/:id/user/:user/exists", projectUserExists) 13 | api.Register("GET", "iot/project/:id/user/:user/bind", projectUserBind) 14 | api.Register("GET", "iot/project/:id/user/:user/unbind", projectUserUnbind) 15 | api.Register("GET", "iot/project/:id/user/:user/disable", projectUserDisable) 16 | api.Register("GET", "iot/project/:id/user/:user/enable", projectUserEnable) 17 | api.Register("POST", "iot/project/:id/user/:user", projectUserUpdate) 18 | //个人项目 19 | api.Register("GET", "/user/:id/projects", userProjects) 20 | } 21 | 22 | // @Summary 项目用户列表 23 | // @Schemes 24 | // @Description 项目用户列表 25 | // @Tags project-user 26 | // @Param id path int true "项目ID" 27 | // @Accept json 28 | // @Produce json 29 | // @Success 200 {object} curd.ReplyData[[]ProjectUser] 返回项目用户信息 30 | // @Router iot/project/{id}/user/list [get] 31 | func projectUserList(ctx *gin.Context) { 32 | var pds []ProjectUser 33 | err := db.Engine(). 34 | Select("project_user.project_id, project_user.user_id, project_user.admin, project_user.disabled, project_user.created, user.name as user"). 35 | Join("INNER", "user", "user.id=project_user.user_id"). 36 | Where("project_user.project_id=?", ctx.Param("id")). 37 | Find(&pds) 38 | if err != nil { 39 | api.Error(ctx, err) 40 | return 41 | } 42 | api.OK(ctx, pds) 43 | } 44 | 45 | // @Summary 判断项目用户是否存在 46 | // @Schemes 47 | // @Description 判断项目用户是否存在 48 | // @Tags project-user 49 | // @Param id path int true "项目ID" 50 | // @Param user path int true "用户ID" 51 | // @Accept json 52 | // @Produce json 53 | // @Success 200 {object} curd.ReplyData[bool] 54 | // @Router iot/project/{id}/user/{user}/exists [get] 55 | func projectUserExists(ctx *gin.Context) { 56 | pd := ProjectUser{ 57 | ProjectId: ctx.Param("id"), 58 | UserId: ctx.Param("user"), 59 | } 60 | has, err := db.Engine().Exist(&pd) 61 | if err != nil { 62 | api.Error(ctx, err) 63 | return 64 | } 65 | api.OK(ctx, has) 66 | } 67 | 68 | // @Summary 绑定项目用户 69 | // @Schemes 70 | // @Description 绑定项目用户 71 | // @Tags project-user 72 | // @Param id path int true "项目ID" 73 | // @Param user path int true "用户ID" 74 | // @Accept json 75 | // @Produce json 76 | // @Success 200 {object} curd.ReplyData[int] 77 | // @Router iot/project/{id}/user/{user}/bind [get] 78 | func projectUserBind(ctx *gin.Context) { 79 | pd := ProjectUser{ 80 | ProjectId: ctx.Param("id"), 81 | UserId: ctx.Param("user"), 82 | } 83 | _, err := db.Engine().InsertOne(&pd) 84 | if err != nil { 85 | api.Error(ctx, err) 86 | return 87 | } 88 | api.OK(ctx, nil) 89 | } 90 | 91 | // @Summary 删除项目用户 92 | // @Schemes 93 | // @Description 删除项目用户 94 | // @Tags project-user 95 | // @Param id path int true "项目ID" 96 | // @Param user path int true "用户ID" 97 | // @Accept json 98 | // @Produce json 99 | // @Success 200 {object} curd.ReplyData[int] 100 | // @Router iot/project/{id}/user/{user}/unbind [get] 101 | func projectUserUnbind(ctx *gin.Context) { 102 | _, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("user")}).Delete(new(ProjectUser)) 103 | if err != nil { 104 | api.Error(ctx, err) 105 | return 106 | } 107 | api.OK(ctx, nil) 108 | } 109 | 110 | // @Summary 禁用项目用户 111 | // @Schemes 112 | // @Description 禁用项目用户 113 | // @Tags project-user 114 | // @Param id path int true "项目ID" 115 | // @Param user path int true "用户ID" 116 | // @Accept json 117 | // @Produce json 118 | // @Success 200 {object} curd.ReplyData[int] 119 | // @Router iot/project/{id}/user/{user}/disable [get] 120 | func projectUserDisable(ctx *gin.Context) { 121 | pd := ProjectUser{Disabled: true} 122 | _, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("user")}).Cols("disabled").Update(&pd) 123 | if err != nil { 124 | api.Error(ctx, err) 125 | return 126 | } 127 | api.OK(ctx, nil) 128 | } 129 | 130 | // @Summary 启用项目用户 131 | // @Schemes 132 | // @Description 启用项目用户 133 | // @Tags project-user 134 | // @Param id path int true "项目ID" 135 | // @Param user path int true "用户ID" 136 | // @Accept json 137 | // @Produce json 138 | // @Success 200 {object} curd.ReplyData[int] 139 | // @Router iot/project/{id}/user/{user}/enable [get] 140 | func projectUserEnable(ctx *gin.Context) { 141 | pd := ProjectUser{Disabled: false} 142 | _, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("user")}).Cols("disabled").Update(&pd) 143 | if err != nil { 144 | api.Error(ctx, err) 145 | return 146 | } 147 | api.OK(ctx, nil) 148 | } 149 | 150 | // @Summary 修改项目用户 151 | // @Schemes 152 | // @Description 修改项目用户 153 | // @Tags project-user 154 | // @Param id path int true "项目ID" 155 | // @Param user path int true "用户ID" 156 | // @Param project-user body ProjectUser true "项目用户信息" 157 | // @Accept json 158 | // @Produce json 159 | // @Success 200 {object} curd.ReplyData[int] 160 | // @Router iot/project/{id}/user/{user} [post] 161 | func projectUserUpdate(ctx *gin.Context) { 162 | var pd ProjectUser 163 | err := ctx.ShouldBindJSON(&pd) 164 | if err != nil { 165 | api.Error(ctx, err) 166 | return 167 | } 168 | _, err = db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("user")}). 169 | Cols("user_id", "disabled"). 170 | Update(&pd) 171 | if err != nil { 172 | api.Error(ctx, err) 173 | return 174 | } 175 | api.OK(ctx, nil) 176 | } 177 | 178 | // @Summary 获取用户的项目列表 179 | // @Schemes 180 | // @Description 获取用户的项目列表 181 | // @Tags user 182 | // @Accept json 183 | // @Produce json 184 | // @Success 200 {object} curd.ReplyData[[]Project] 返回项目列表 185 | // @Router /user/{id}iot/projects [get] 186 | func userProjects(ctx *gin.Context) { 187 | id := ctx.GetString("id") 188 | 189 | var projects []*Project 190 | err := db.Engine().Join("INNER", "project_user", "project_user.project_id=id"). 191 | Where("project_user.user_id=?", id).Find(&projects) 192 | if err != nil { 193 | api.Error(ctx, err) 194 | return 195 | } 196 | 197 | api.OK(ctx, projects) 198 | } 199 | -------------------------------------------------------------------------------- /protocol/api.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/busy-cloud/boat/api" 6 | "github.com/gin-gonic/gin" 7 | "path/filepath" 8 | ) 9 | 10 | func init() { 11 | 12 | api.Register("GET", "iot/protocol/list", func(ctx *gin.Context) { 13 | var ps []*Base 14 | for _, item := range protocolsStore.Items { 15 | entries, err := item.ReadDir("") 16 | if err != nil { 17 | api.Error(ctx, err) 18 | return 19 | } 20 | for _, entry := range entries { 21 | if entry.IsDir() { 22 | continue 23 | } 24 | ext := filepath.Ext(entry.Name()) 25 | if ext == ".json" { 26 | buf, err := item.ReadFile(entry.Name()) 27 | if err != nil { 28 | api.Error(ctx, err) 29 | return 30 | } 31 | var menu Base 32 | err = json.Unmarshal(buf, &menu) 33 | if err != nil { 34 | api.Error(ctx, err) 35 | return 36 | } 37 | 38 | ps = append(ps, &menu) 39 | } 40 | } 41 | } 42 | api.OK(ctx, ps) 43 | }) 44 | 45 | api.Register("GET", "iot/protocol/:name", func(ctx *gin.Context) { 46 | name := ctx.Param("name") 47 | ctx.FileFromFS(name+".json", &protocolsStore) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /protocol/protocol.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/smart" 5 | ) 6 | 7 | type Base struct { 8 | Name string `json:"name"` 9 | Description string `json:"description,omitempty"` 10 | Version string `json:"version,omitempty"` 11 | Author string `json:"author,omitempty"` 12 | Copyright string `json:"copyright,omitempty"` 13 | } 14 | 15 | type Protocol struct { 16 | Base 17 | 18 | Station *smart.Form `json:"station,omitempty"` //从站信息 19 | Options *smart.Form `json:"options,omitempty"` //协议参数 20 | Model *smart.Form `json:"model,omitempty"` //模型配置文件 21 | } 22 | -------------------------------------------------------------------------------- /protocol/store.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "embed" 5 | "github.com/busy-cloud/boat/store" 6 | ) 7 | 8 | var protocolsStore store.Store 9 | 10 | func Dir(dir string) { 11 | protocolsStore.AddDir(dir) 12 | } 13 | 14 | func Zip(zip string) { 15 | protocolsStore.AddZip(zip) 16 | } 17 | 18 | func EmbedFS(fs *embed.FS, base string) { 19 | protocolsStore.Add(store.PrefixFS(fs, base)) 20 | } 21 | -------------------------------------------------------------------------------- /protocols/modbus.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "modbus", 3 | "description": "Modbus RTU", 4 | "station": [ 5 | { 6 | "key": "slave", 7 | "label": "从站号", 8 | "type": "number", 9 | "default": 1, 10 | "min": 1, 11 | "max": 255, 12 | "step": 1 13 | } 14 | ], 15 | "options": [ 16 | { 17 | "key": "tcp", 18 | "type": "switch", 19 | "label": "TCP模式" 20 | }, 21 | { 22 | "key": "timeout", 23 | "label": "超时", 24 | "type": "number", 25 | "default": 1000, 26 | "min": 200, 27 | "max": 5000, 28 | "step": 100 29 | }, 30 | { 31 | "key": "polling", 32 | "type": "switch", 33 | "label": "开启轮询" 34 | }, 35 | { 36 | "key": "polling_interval", 37 | "type": "number", 38 | "label": "轮询间隔", 39 | "default": 60, 40 | "min": 1, 41 | "step": 1 42 | } 43 | ], 44 | "model": [ 45 | { 46 | "key": "crontab", 47 | "label": "定时", 48 | "type": "text" 49 | }, 50 | { 51 | "key": "interval", 52 | "label": "定时间隔s", 53 | "type": "number", 54 | "default": 60, 55 | "min": 1, 56 | "max": 36000, 57 | "step": 1 58 | }, 59 | { 60 | "key": "timeout", 61 | "label": "超时 ms", 62 | "type": "number", 63 | "default": 1000, 64 | "min": 200, 65 | "max": 5000, 66 | "step": 100 67 | }, 68 | { 69 | "key": "mapper", 70 | "label": "映射表", 71 | "type": "object", 72 | "children": [ 73 | { 74 | "key": "coils", 75 | "label": "线圈 01", 76 | "type": "table", 77 | "children": [ 78 | { 79 | "key": "name", 80 | "label": "变量", 81 | "type": "text" 82 | }, 83 | { 84 | "key": "address", 85 | "label": "地址", 86 | "type": "number", 87 | "default": 0, 88 | "min": 0, 89 | "step": 1 90 | } 91 | ] 92 | }, 93 | { 94 | "key": "discrete_inputs", 95 | "label": "离散输入 02", 96 | "type": "table", 97 | "children": [ 98 | { 99 | "key": "name", 100 | "label": "变量", 101 | "type": "text" 102 | }, 103 | { 104 | "key": "address", 105 | "label": "地址", 106 | "type": "number", 107 | "default": 0, 108 | "min": 0, 109 | "step": 1 110 | } 111 | ] 112 | }, 113 | { 114 | "key": "holding_registers", 115 | "label": "保持寄存器 03", 116 | "type": "table", 117 | "children": [ 118 | { 119 | "key": "name", 120 | "label": "变量", 121 | "type": "text" 122 | }, 123 | { 124 | "key": "type", 125 | "label": "类型", 126 | "type": "select", 127 | "default": "uint16", 128 | "options": [ 129 | { 130 | "label": "int16", 131 | "value": "int16" 132 | }, 133 | { 134 | "label": "uint16", 135 | "value": "uint16" 136 | }, 137 | { 138 | "label": "int32", 139 | "value": "int32" 140 | }, 141 | { 142 | "label": "uint32", 143 | "value": "uint32" 144 | }, 145 | { 146 | "label": "float32", 147 | "value": "float32" 148 | }, 149 | { 150 | "label": "float64", 151 | "value": "float64" 152 | } 153 | ] 154 | }, 155 | { 156 | "key": "address", 157 | "label": "地址", 158 | "type": "number", 159 | "default": 0, 160 | "min": 0, 161 | "step": 1 162 | }, 163 | { 164 | "key": "be", 165 | "label": "大端模式", 166 | "type": "switch", 167 | "default": true 168 | }, 169 | { 170 | "key": "rate", 171 | "label": "倍率", 172 | "type": "number", 173 | "default": 1 174 | }, 175 | { 176 | "key": "correct", 177 | "label": "校准", 178 | "type": "number", 179 | "default": 0 180 | } 181 | ] 182 | }, 183 | { 184 | "key": "input_registers", 185 | "label": "输入寄存器 04", 186 | "type": "table", 187 | "children": [ 188 | { 189 | "key": "name", 190 | "label": "变量", 191 | "type": "text" 192 | }, 193 | { 194 | "key": "type", 195 | "label": "类型", 196 | "type": "select", 197 | "default": "uint16", 198 | "options": [ 199 | { 200 | "label": "int16", 201 | "value": "int16" 202 | }, 203 | { 204 | "label": "uint16", 205 | "value": "uint16" 206 | }, 207 | { 208 | "label": "int32", 209 | "value": "int32" 210 | }, 211 | { 212 | "label": "uint32", 213 | "value": "uint32" 214 | }, 215 | { 216 | "label": "float32", 217 | "value": "float32" 218 | }, 219 | { 220 | "label": "float64", 221 | "value": "float64" 222 | } 223 | ] 224 | }, 225 | { 226 | "key": "address", 227 | "label": "地址", 228 | "type": "number", 229 | "default": 0, 230 | "min": 0, 231 | "step": 1 232 | }, 233 | { 234 | "key": "be", 235 | "label": "大端模式", 236 | "type": "switch", 237 | "default": true 238 | }, 239 | { 240 | "key": "rate", 241 | "label": "倍率", 242 | "type": "number", 243 | "default": 1 244 | }, 245 | { 246 | "key": "correct", 247 | "label": "校准", 248 | "type": "number", 249 | "default": 0 250 | } 251 | ] 252 | } 253 | ] 254 | }, 255 | { 256 | "key": "pollers", 257 | "type": "table", 258 | "label": "轮询器", 259 | "children": [ 260 | { 261 | "key": "code", 262 | "label": "类型 功能码", 263 | "type": "select", 264 | "options": [ 265 | { 266 | "label": "线圈 1", 267 | "value": 1 268 | }, 269 | { 270 | "label": "离散输入 2", 271 | "value": 2 272 | }, 273 | { 274 | "label": "保持寄存器 3", 275 | "value": 3 276 | }, 277 | { 278 | "label": "输入寄存器 4", 279 | "value": 4 280 | } 281 | ] 282 | }, 283 | { 284 | "key": "address", 285 | "label": "地址", 286 | "type": "number", 287 | "default": 0, 288 | "min": 0, 289 | "step": 1 290 | }, 291 | { 292 | "key": "length", 293 | "label": "长度", 294 | "type": "number", 295 | "default": 1, 296 | "min": 1, 297 | "step": 1 298 | } 299 | ] 300 | } 301 | ] 302 | } -------------------------------------------------------------------------------- /space/device-api.go: -------------------------------------------------------------------------------- 1 | package space 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/api" 5 | "github.com/busy-cloud/boat/db" 6 | "github.com/gin-gonic/gin" 7 | "xorm.io/xorm/schemas" 8 | ) 9 | 10 | func init() { 11 | api.Register("GET", "iot/space/:id/device/list", spaceDeviceList) 12 | api.Register("GET", "iot/space/:id/device/:device/bind", spaceDeviceBind) 13 | api.Register("GET", "iot/space/:id/device/:device/unbind", spaceDeviceUnbind) 14 | api.Register("POST", "iot/space/:id/device/:device", spaceDeviceUpdate) 15 | } 16 | 17 | // @Summary 空间设备列表 18 | // @Schemes 19 | // @Description 空间设备列表 20 | // @Tags space-device 21 | // @Param id path int true "项目ID" 22 | // @Accept json 23 | // @Produce json 24 | // @Success 200 {object} curd.ReplyData[[]SpaceDevice] 返回空间设备信息 25 | // @Router iot/space/{id}/device/list [get] 26 | func spaceDeviceList(ctx *gin.Context) { 27 | var pds []SpaceDevice 28 | err := db.Engine(). 29 | Select("space_device.space_id, space_device.device_id, space_device.created, device.name as device"). 30 | Join("INNER", "device", "device.id=space_device.device_id"). 31 | Where("space_device.space_id=?", ctx.Param("id")). 32 | Find(&pds) 33 | if err != nil { 34 | api.Error(ctx, err) 35 | return 36 | } 37 | api.OK(ctx, pds) 38 | } 39 | 40 | // @Summary 绑定空间设备 41 | // @Schemes 42 | // @Description 绑定空间设备 43 | // @Tags space-device 44 | // @Param id path int true "项目ID" 45 | // @Param device path int true "设备ID" 46 | // @Accept json 47 | // @Produce json 48 | // @Success 200 {object} curd.ReplyData[int] 49 | // @Router iot/space/{id}/device/{device}/bind [get] 50 | func spaceDeviceBind(ctx *gin.Context) { 51 | pd := SpaceDevice{ 52 | SpaceId: ctx.Param("id"), 53 | DeviceId: ctx.Param("device"), 54 | } 55 | _, err := db.Engine().InsertOne(&pd) 56 | if err != nil { 57 | api.Error(ctx, err) 58 | return 59 | } 60 | api.OK(ctx, nil) 61 | } 62 | 63 | // @Summary 删除空间设备 64 | // @Schemes 65 | // @Description 删除空间设备 66 | // @Tags space-device 67 | // @Param id path int true "项目ID" 68 | // @Param device path int true "设备ID" 69 | // @Accept json 70 | // @Produce json 71 | // @Success 200 {object} curd.ReplyData[int] 72 | // @Router iot/space/{id}/device/{device}/unbind [get] 73 | func spaceDeviceUnbind(ctx *gin.Context) { 74 | _, err := db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("device")}).Delete(new(SpaceDevice)) 75 | if err != nil { 76 | api.Error(ctx, err) 77 | return 78 | } 79 | api.OK(ctx, nil) 80 | } 81 | 82 | // @Summary 修改空间设备 83 | // @Schemes 84 | // @Description 修改空间设备 85 | // @Tags space-device 86 | // @Param id path int true "项目ID" 87 | // @Param device path int true "设备ID" 88 | // @Param space-device body SpaceDevice true "空间设备信息" 89 | // @Accept json 90 | // @Produce json 91 | // @Success 200 {object} curd.ReplyData[int] 92 | // @Router iot/space/{id}/device/{device} [post] 93 | func spaceDeviceUpdate(ctx *gin.Context) { 94 | var pd SpaceDevice 95 | err := ctx.ShouldBindJSON(&pd) 96 | if err != nil { 97 | api.Error(ctx, err) 98 | return 99 | } 100 | _, err = db.Engine().ID(schemas.PK{ctx.Param("id"), ctx.Param("device")}). 101 | Cols("device_id", "name", "disabled"). 102 | Update(&pd) 103 | if err != nil { 104 | api.Error(ctx, err) 105 | return 106 | } 107 | api.OK(ctx, nil) 108 | } 109 | -------------------------------------------------------------------------------- /space/space-api.go: -------------------------------------------------------------------------------- 1 | package space 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/api" 5 | "github.com/busy-cloud/boat/curd" 6 | ) 7 | 8 | func init() { 9 | 10 | api.Register("POST", "iot/space/count", curd.ApiCount[Space]()) 11 | 12 | api.Register("POST", "iot/space/search", curd.ApiSearchWith[Space]([]*curd.With{ 13 | {"space", "parent_id", "id", "name", "parent"}, 14 | }, "id", "name", "project_id", "parent_id", "description", "disabled", "created")) 15 | api.Register("GET", "iot/space/list", curd.ApiList[Space]()) 16 | api.Register("POST", "iot/space/create", curd.ApiCreate[Space]()) 17 | api.Register("GET", "iot/space/:id", curd.ApiGet[Space]()) 18 | api.Register("POST", "iot/space/:id", curd.ApiUpdate[Space]()) 19 | api.Register("GET", "iot/space/:id/delete", curd.ApiDelete[Space]()) 20 | api.Register("GET", "iot/space/:id/disable", curd.ApiDisable[Space](true)) 21 | api.Register("GET", "iot/space/:id/enable", curd.ApiDisable[Space](false)) 22 | } 23 | 24 | // @Summary 查询空间 25 | // @Schemes 26 | // @Description 查询空间 27 | // @Tags space 28 | // @Param search body curd.ParamSearch true "查询参数" 29 | // @Accept json 30 | // @Produce json 31 | // @Success 200 {object} curd.ReplyList[Space] 返回空间信息 32 | // @Router iot/space/search [post] 33 | func noopSpaceSearch() {} 34 | 35 | // @Summary 查询空间 36 | // @Schemes 37 | // @Description 查询空间 38 | // @Tags space 39 | // @Param search query curd.ParamList true "查询参数" 40 | // @Accept json 41 | // @Produce json 42 | // @Success 200 {object} curd.ReplyList[Space] 返回空间信息 43 | // @Router iot/space/list [get] 44 | func noopSpaceList() {} 45 | 46 | // @Summary 创建空间 47 | // @Schemes 48 | // @Description 创建空间 49 | // @Tags space 50 | // @Param search body Space true "空间信息" 51 | // @Accept json 52 | // @Produce json 53 | // @Success 200 {object} curd.ReplyData[Space] 返回空间信息 54 | // @Router iot/space/create [post] 55 | func noopSpaceCreate() {} 56 | 57 | // @Summary 修改空间 58 | // @Schemes 59 | // @Description 修改空间 60 | // @Tags space 61 | // @Param id path int true "空间ID" 62 | // @Param space body Space true "空间信息" 63 | // @Accept json 64 | // @Produce json 65 | // @Success 200 {object} curd.ReplyData[Space] 返回空间信息 66 | // @Router iot/space/{id} [post] 67 | func noopSpaceUpdate() {} 68 | 69 | // @Summary 删除空间 70 | // @Schemes 71 | // @Description 删除空间 72 | // @Tags space 73 | // @Param id path int true "空间ID" 74 | // @Accept json 75 | // @Produce json 76 | // @Success 200 {object} curd.ReplyData[Space] 返回空间信息 77 | // @Router iot/space/{id}/delete [get] 78 | func noopSpaceDelete() {} 79 | 80 | // @Summary 启用空间 81 | // @Schemes 82 | // @Description 启用空间 83 | // @Tags space 84 | // @Param id path int true "空间ID" 85 | // @Accept json 86 | // @Produce json 87 | // @Success 200 {object} curd.ReplyData[Space] 返回空间信息 88 | // @Router iot/space/{id}/enable [get] 89 | func noopSpaceEnable() {} 90 | 91 | // @Summary 禁用空间 92 | // @Schemes 93 | // @Description 禁用空间 94 | // @Tags space 95 | // @Param id path int true "空间ID" 96 | // @Accept json 97 | // @Produce json 98 | // @Success 200 {object} curd.ReplyData[Space] 返回空间信息 99 | // @Router iot/space/{id}/disable [get] 100 | func noopSpaceDisable() {} 101 | -------------------------------------------------------------------------------- /space/space.go: -------------------------------------------------------------------------------- 1 | package space 2 | 3 | import ( 4 | "github.com/busy-cloud/boat/db" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | db.Register(new(Space), new(SpaceDevice)) 10 | } 11 | 12 | type SpaceDevice struct { 13 | SpaceId string `json:"space_id,omitempty" xorm:"pk"` 14 | Space string `json:"space,omitempty" xorm:"<-"` 15 | DeviceId string `json:"device_id,omitempty" xorm:"pk"` 16 | Device string `json:"device,omitempty" xorm:"<-"` 17 | Created time.Time `json:"created" xorm:"created"` 18 | } 19 | 20 | type Space struct { 21 | Id string `json:"id" xorm:"pk"` 22 | Name string `json:"name,omitempty"` //名称 23 | Description string `json:"description,omitempty"` //说明 24 | 25 | ProjectId string `json:"project_id,omitempty" xorm:"index"` 26 | Project string `json:"project,omitempty" xorm:"<-"` 27 | ParentId string `json:"parent_id,omitempty" xorm:"index"` 28 | Parent string `json:"parent,omitempty" xorm:"<-"` 29 | 30 | Disabled bool `json:"disabled,omitempty"` 31 | Created time.Time `json:"created" xorm:"created"` 32 | } 33 | -------------------------------------------------------------------------------- /test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "encoding/json" 6 | _ "github.com/busy-cloud/boat/apis" 7 | "github.com/busy-cloud/boat/apps" 8 | "github.com/busy-cloud/boat/boot" 9 | _ "github.com/busy-cloud/boat/broker" 10 | "github.com/busy-cloud/boat/log" 11 | "github.com/busy-cloud/boat/store" 12 | _ "github.com/busy-cloud/boat/table" 13 | "github.com/busy-cloud/boat/web" 14 | _ "github.com/busy-cloud/connector" 15 | _ "github.com/busy-cloud/dash" 16 | _ "github.com/busy-cloud/influxdb" 17 | _ "github.com/busy-cloud/modbus" 18 | _ "github.com/busy-cloud/noob" 19 | _ "github.com/busy-cloud/user" 20 | _ "github.com/god-jason/iot-master" 21 | "github.com/god-jason/iot-master/protocol" 22 | "github.com/spf13/viper" 23 | "os" 24 | "os/signal" 25 | "syscall" 26 | ) 27 | 28 | func init() { 29 | manifest, err := os.ReadFile("manifest.json") 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | //注册为内部插件 35 | var a apps.App 36 | err = json.Unmarshal(manifest, &a) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | apps.Register(&a) 41 | 42 | //注册资源 43 | a.AssetsFS = store.Dir("assets") 44 | a.PagesFS = store.Dir("pages") 45 | 46 | //协议 47 | protocol.Dir("protocols") 48 | } 49 | 50 | func main() { 51 | viper.SetConfigName("iot-master") 52 | 53 | sigs := make(chan os.Signal, 1) 54 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 55 | go func() { 56 | <-sigs 57 | 58 | //关闭web,出发 59 | _ = web.Shutdown() 60 | }() 61 | 62 | //安全退出 63 | defer boot.Shutdown() 64 | 65 | err := boot.Startup() 66 | if err != nil { 67 | log.Fatal(err) 68 | return 69 | } 70 | 71 | err = web.Serve() 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | } 76 | --------------------------------------------------------------------------------