├── .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 | [](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 |
--------------------------------------------------------------------------------