├── pages
├── tsconfig.json
├── src
│ ├── content
│ │ ├── docs
│ │ │ ├── developer
│ │ │ │ ├── export.md
│ │ │ │ └── plugin.md
│ │ │ ├── library
│ │ │ │ └── model.mdx
│ │ │ ├── index.mdx
│ │ │ └── guides
│ │ │ │ └── about.mdx
│ │ └── config.ts
│ ├── env.d.ts
│ └── assets
│ │ └── houston.webp
├── .vscode
│ ├── extensions.json
│ └── launch.json
├── package.json
├── public
│ └── favicon.svg
├── astro.config.mjs
└── README.md
├── internal
├── plugins
│ ├── bacnet
│ │ ├── bacnet
│ │ │ ├── encoding
│ │ │ │ ├── object.go
│ │ │ │ ├── error.go
│ │ │ │ ├── types.go
│ │ │ │ ├── const.go
│ │ │ │ ├── bvlc.go
│ │ │ │ ├── validate.go
│ │ │ │ ├── writemultiple.go
│ │ │ │ ├── readmultiple.go
│ │ │ │ ├── iam.go
│ │ │ │ ├── string.go
│ │ │ │ ├── writeprop.go
│ │ │ │ ├── whois.go
│ │ │ │ └── date.go
│ │ │ ├── network
│ │ │ │ ├── err.go
│ │ │ │ ├── whois.go
│ │ │ │ ├── write.go
│ │ │ │ ├── discover_test.go
│ │ │ │ ├── base.go
│ │ │ │ ├── strings_test.go
│ │ │ │ ├── device.go
│ │ │ │ ├── strings.go
│ │ │ │ └── read.go
│ │ │ ├── cmd
│ │ │ │ ├── main.go
│ │ │ │ └── cmd
│ │ │ │ │ ├── whois_test.go
│ │ │ │ │ ├── root.go
│ │ │ │ │ └── whoIs.go
│ │ │ ├── btypes
│ │ │ │ ├── null
│ │ │ │ │ └── null.go
│ │ │ │ ├── const.go
│ │ │ │ ├── ndpu
│ │ │ │ │ ├── messagetypes.go
│ │ │ │ │ └── networkmessagetype_string.go
│ │ │ │ ├── segmentation
│ │ │ │ │ ├── segmentation.go
│ │ │ │ │ └── segmentedtype_string.go
│ │ │ │ ├── npdu.go
│ │ │ │ ├── marshal_test.go
│ │ │ │ ├── bvlc.go
│ │ │ │ ├── bacerr
│ │ │ │ │ └── errorclass_string.go
│ │ │ │ ├── datetime.go
│ │ │ │ ├── object_map.go
│ │ │ │ ├── services
│ │ │ │ │ └── services_test.go
│ │ │ │ └── address.go
│ │ │ ├── utsm
│ │ │ │ ├── doc.go
│ │ │ │ ├── main_test.go
│ │ │ │ └── subscriber.go
│ │ │ ├── helpers
│ │ │ │ ├── ipbytes
│ │ │ │ │ ├── ip_test.go
│ │ │ │ │ └── ip.go
│ │ │ │ ├── print
│ │ │ │ │ └── console.go
│ │ │ │ ├── validation
│ │ │ │ │ ├── validation_test.go
│ │ │ │ │ └── vaildation.go
│ │ │ │ ├── store
│ │ │ │ │ └── init.go
│ │ │ │ ├── data
│ │ │ │ │ └── data.go
│ │ │ │ └── nils
│ │ │ │ │ └── nil.go
│ │ │ ├── datalink
│ │ │ │ └── datalink.go
│ │ │ ├── const.go
│ │ │ ├── misc.go
│ │ │ ├── iam_test.go
│ │ │ ├── iam.go
│ │ │ ├── whoisrouter.go
│ │ │ ├── whatnetwork.go
│ │ │ ├── tsm
│ │ │ │ └── transactions_test.go
│ │ │ ├── writeprop.go
│ │ │ ├── readprop.go
│ │ │ └── writemulti.go
│ │ ├── mock.go
│ │ └── plugin.go
│ ├── modbus
│ │ ├── rest_api.go
│ │ └── mock.go
│ ├── dlt645
│ │ ├── core
│ │ │ ├── error.go
│ │ │ ├── buffer.go
│ │ │ ├── crc_test.go
│ │ │ ├── crc.go
│ │ │ ├── dltcon
│ │ │ │ ├── option.go
│ │ │ │ └── proc.go
│ │ │ ├── res
│ │ │ │ └── DataMarkerConfig.toml
│ │ │ ├── log.go
│ │ │ └── serial.go
│ │ ├── mock.go
│ │ ├── adapter.go
│ │ └── model.go
│ ├── mirror
│ │ └── model.go
│ ├── manage.go
│ ├── tcpserver
│ │ └── plugin.go
│ ├── httpserver
│ │ └── plugin.go
│ ├── websocket
│ │ └── plugin.go
│ ├── httpclient
│ │ └── plugin.go
│ └── gwplugin
│ │ └── gwplugin.go
├── export
│ ├── mirror
│ │ └── model.go
│ ├── discover
│ │ └── model.go
│ ├── export.go
│ ├── gwexport
│ │ └── gwexport.go
│ ├── ai
│ │ ├── mcp
│ │ │ └── tools
│ │ │ │ ├── shadow.go
│ │ │ │ ├── driver_box.go
│ │ │ │ └── history.go
│ │ ├── mcp_client.go
│ │ └── coordinator_agent.go
│ ├── ui
│ │ └── export.go
│ └── basic
│ │ └── export.go
├── logger
│ ├── chan_writer.go
│ └── logger.go
├── dto
│ └── ws.go
└── core
│ └── common.go
├── driverbox
├── export
│ ├── linkedge
│ │ ├── linkedge.go
│ │ ├── trigger.go
│ │ ├── config.go
│ │ ├── action.go
│ │ └── validator.go
│ ├── export.go
│ └── mqtt_export.go
├── restful
│ ├── request
│ │ └── shadow.go
│ ├── response
│ │ └── common.go
│ ├── error.go
│ ├── route
│ │ └── api.go
│ └── restful.go
├── models
│ └── api.go
├── helper
│ ├── logger.go
│ ├── crontab
│ │ ├── crontab_test.go
│ │ └── crontab.go
│ ├── helper.go
│ └── script.go
├── plugin.go
├── library
│ ├── mirror_tpl.go
│ ├── device_model.go
│ └── tag.go
├── pkg
│ └── mbserver
│ │ ├── internal
│ │ └── storage.go
│ │ ├── modbus
│ │ ├── storage_test.go
│ │ ├── server.go
│ │ ├── slave.go
│ │ └── storage.go
│ │ ├── register.go
│ │ └── common.go
├── plugin
│ ├── plugin.go
│ └── model.go
├── event
│ └── event.go
└── export.go
├── exports
├── mcp
│ └── export.go
├── ui
│ └── export.go
├── basic
│ └── export.go
├── gateway
│ └── export.go
├── mirror
│ └── export.go
├── linkedge
│ └── export.go
├── discover
│ └── export.go
└── export.go
├── test
├── base_test.go
└── library_test.go
├── plugins
├── bacnet
│ └── plugin.go
├── dlt645
│ └── plugin.go
├── gateway
│ └── plugin.go
├── mqtt
│ └── plugin.go
├── mirror
│ └── plugin.go
├── modbus
│ └── plugin.go
├── websocket
│ └── plugin.go
├── httpclient
│ └── plugin.go
├── httpserver
│ └── plugin.go
├── tcpserver
│ └── plugin.go
└── plugin_all.go
├── res
├── library
│ ├── index.json
│ ├── driver
│ │ ├── test_1.lua
│ │ └── test_2.lua
│ └── protocol
│ │ └── ws_demo.lua
├── driver
│ ├── mirror
│ │ └── config.json
│ ├── http_server
│ │ ├── converter.lua
│ │ └── config.json
│ └── websocket
│ │ └── config.json
└── ui
│ └── devices.tmpl
├── main.go
├── .gitignore
├── README.en.md
├── Dockerfile
├── .github
└── workflows
│ └── pages.yml
├── deploy.sh
└── README.md
/pages/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict"
3 | }
4 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/object.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | const MaxObject = 0x3FF
4 |
--------------------------------------------------------------------------------
/pages/src/content/docs/developer/export.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Export开发
3 | sidebar:
4 | order: 3
5 | ---
--------------------------------------------------------------------------------
/pages/src/content/docs/developer/plugin.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 通讯插件开发
3 | sidebar:
4 | order: 2
5 | ---
--------------------------------------------------------------------------------
/driverbox/export/linkedge/linkedge.go:
--------------------------------------------------------------------------------
1 | package linkedge
2 |
3 | // todo 场景联动开放 API(create、update、delete……)
4 |
--------------------------------------------------------------------------------
/pages/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/pages/src/assets/houston.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ibuilding-X/driver-box/HEAD/pages/src/assets/houston.webp
--------------------------------------------------------------------------------
/pages/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/pages/src/content/docs/library/model.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: model-物模型
3 | description: A reference page in my new Starlight docs site.
4 | ---
5 |
6 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/network/err.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import "errors"
4 |
5 | var (
6 | ObjectNil = errors.New("object list can not be empty")
7 | )
8 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/cmd/cmd"
4 |
5 | func main() {
6 | cmd.Execute()
7 | }
8 |
--------------------------------------------------------------------------------
/internal/plugins/modbus/rest_api.go:
--------------------------------------------------------------------------------
1 | package modbus
2 |
3 | func InitRestAPI() {
4 | //restful.HandleFunc("", func(request *http.Request) (any, error) {
5 | // return nil, nil
6 | //})
7 | }
8 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/null/null.go:
--------------------------------------------------------------------------------
1 | package null
2 |
3 | // Null is used when a value is empty.
4 | type Null struct{}
5 |
6 | func (n Null) String() string {
7 | return ""
8 | }
9 |
--------------------------------------------------------------------------------
/pages/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection } from 'astro:content';
2 | import { docsSchema } from '@astrojs/starlight/schema';
3 |
4 | export const collections = {
5 | docs: defineCollection({ schema: docsSchema() }),
6 | };
7 |
--------------------------------------------------------------------------------
/exports/mcp/export.go:
--------------------------------------------------------------------------------
1 | package mcp
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox"
5 | "github.com/ibuilding-x/driver-box/internal/export/ai"
6 | )
7 |
8 | func LoadMcpExport() {
9 | driverbox.Exports.LoadExport(ai.NewExport())
10 | }
11 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/core/error.go:
--------------------------------------------------------------------------------
1 | package dlt645
2 |
3 | import (
4 | "errors"
5 | )
6 |
7 | // ErrClosedConnection 连接已关闭
8 | var ErrClosedConnection = errors.New("use of closed connection")
9 | var ErrConnectionFailed = errors.New("create connection failed")
10 |
--------------------------------------------------------------------------------
/pages/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/test/base_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/config"
5 | "github.com/ibuilding-x/driver-box/internal/logger"
6 | )
7 |
8 | func Init() {
9 | config.ResourcePath = "../res"
10 | logger.InitLogger("", "debug")
11 | }
12 |
--------------------------------------------------------------------------------
/driverbox/restful/request/shadow.go:
--------------------------------------------------------------------------------
1 | package request
2 |
3 | type UpdateDeviceReq []UpdateDeviceData
4 |
5 | // UpdateDeviceData 更新设备点位请求数据
6 | type UpdateDeviceData struct {
7 | ID string `json:"id"`
8 | Name string `json:"name"`
9 | Value any `json:"value"`
10 | }
11 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/utsm/doc.go:
--------------------------------------------------------------------------------
1 | package utsm
2 |
3 | /*
4 | utsm is the Unconfirmed Transaction State Manager. These types of
5 | transactions do not necessarily have a single destination but rather multiple
6 | destinations. Using this library, we set up a simple pub-sub model.
7 | */
8 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/helpers/ipbytes/ip_test.go:
--------------------------------------------------------------------------------
1 | package ip2bytes
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestIP(t *testing.T) {
9 |
10 | mac, err := New("192.168.15.10", 47808)
11 | if err != nil {
12 | return
13 | }
14 |
15 | fmt.Println(mac)
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/plugins/bacnet/plugin.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet"
6 | )
7 |
8 | func RegisterPlugin() {
9 | plugins.Manager.Register(bacnet.ProtocolName, new(bacnet.Plugin))
10 | }
11 |
--------------------------------------------------------------------------------
/plugins/dlt645/plugin.go:
--------------------------------------------------------------------------------
1 | package dlt645
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/dlt645"
6 | )
7 |
8 | func RegisterPlugin() {
9 | plugins.Manager.Register(dlt645.ProtocolName, new(dlt645.Plugin))
10 | }
11 |
--------------------------------------------------------------------------------
/plugins/gateway/plugin.go:
--------------------------------------------------------------------------------
1 | package gateway
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/gwplugin"
6 | )
7 |
8 | func RegisterPlugin() {
9 | plugins.Manager.Register(gwplugin.ProtocolName, gwplugin.New())
10 | }
11 |
--------------------------------------------------------------------------------
/plugins/mqtt/plugin.go:
--------------------------------------------------------------------------------
1 | package mqtt
2 |
3 | import (
4 | plugins0 "github.com/ibuilding-x/driver-box/internal/plugins"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/mqtt"
6 | )
7 |
8 | func RegisterPlugin() {
9 | plugins0.Manager.Register(mqtt.ProtocolName, new(mqtt.Plugin))
10 | }
11 |
--------------------------------------------------------------------------------
/driverbox/restful/response/common.go:
--------------------------------------------------------------------------------
1 | package response
2 |
3 | // Common 通用 json 响应
4 | type Common struct {
5 | Success bool `json:"success"` // 是否成功
6 | ErrorCode int `json:"errorCode"` // 错误码
7 | ErrorMsg string `json:"errorMsg"` // 错误信息
8 | Data any `json:"data"` // 数据
9 | }
10 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/const.go:
--------------------------------------------------------------------------------
1 | package btypes
2 |
3 | const (
4 | MaxInstance = 0x3FFFFF
5 | )
6 |
7 | const (
8 | // WhoIsAll is used when scanning a range. Using this as one of the two ranges,
9 | // will scan all available devices
10 | WhoIsAll = -1
11 | ArrayAll = 0xFFFFFFFF
12 | )
13 |
--------------------------------------------------------------------------------
/plugins/mirror/plugin.go:
--------------------------------------------------------------------------------
1 | package mirror
2 |
3 | import (
4 | plugins0 "github.com/ibuilding-x/driver-box/internal/plugins"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/mirror"
6 | )
7 |
8 | func RegisterPlugin() {
9 | plugins0.Manager.Register(mirror.ProtocolName, mirror.NewPlugin())
10 | }
11 |
--------------------------------------------------------------------------------
/plugins/modbus/plugin.go:
--------------------------------------------------------------------------------
1 | package modbus
2 |
3 | import (
4 | plugins0 "github.com/ibuilding-x/driver-box/internal/plugins"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/modbus"
6 | )
7 |
8 | func RegisterPlugin() {
9 | plugins0.Manager.Register(modbus.ProtocolName, new(modbus.Plugin))
10 | }
11 |
--------------------------------------------------------------------------------
/plugins/websocket/plugin.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/websocket"
6 | )
7 |
8 | func RegisterPlugin() {
9 | plugins.Manager.Register(websocket.ProtocolName, new(websocket.Plugin))
10 | }
11 |
--------------------------------------------------------------------------------
/plugins/httpclient/plugin.go:
--------------------------------------------------------------------------------
1 | package httpclient
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/httpclient"
6 | )
7 |
8 | func RegisterPlugin() {
9 | plugins.Manager.Register(httpclient.ProtocolName, new(httpclient.Plugin))
10 | }
11 |
--------------------------------------------------------------------------------
/plugins/httpserver/plugin.go:
--------------------------------------------------------------------------------
1 | package httpserver
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/httpserver"
6 | )
7 |
8 | func RegisterPlugin() {
9 | plugins.Manager.Register(httpserver.ProtocolName, new(httpserver.Plugin))
10 | }
11 |
--------------------------------------------------------------------------------
/driverbox/restful/error.go:
--------------------------------------------------------------------------------
1 | package restful
2 |
3 | import "errors"
4 |
5 | var (
6 | // UndefinedErr 未定义错误
7 | UndefinedErr = errors.New("undefined error")
8 | )
9 |
10 | // errorCodes maps error to its corresponding internal status code.
11 | var errorCodes = map[error]int{
12 | UndefinedErr: 500, // 未定义错误
13 | }
14 |
--------------------------------------------------------------------------------
/plugins/tcpserver/plugin.go:
--------------------------------------------------------------------------------
1 | package tcpserver
2 |
3 | import (
4 | plugins0 "github.com/ibuilding-x/driver-box/internal/plugins"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/tcpserver"
6 | )
7 |
8 | func RegisterPlugin() {
9 | plugins0.Manager.Register(tcpserver.ProtocolName, new(tcpserver.Plugin))
10 | }
11 |
--------------------------------------------------------------------------------
/driverbox/models/api.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/config"
5 | )
6 |
7 | // APIConfig restful API request body
8 | type APIConfig struct {
9 | Key string `json:"key"`
10 | Config config.Config `json:"config"`
11 | Script string `json:"script"`
12 | }
13 |
--------------------------------------------------------------------------------
/exports/ui/export.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox"
5 | "github.com/ibuilding-x/driver-box/internal/export/ui"
6 | )
7 |
8 | // LoadUIExport 加载driver-box内置UI Export插件
9 | // 功能:
10 | //
11 | // 创建并加载ui.NewExport()实例
12 | func LoadExport() {
13 | driverbox.Exports.LoadExport(ui.NewExport())
14 | }
15 |
--------------------------------------------------------------------------------
/internal/plugins/mirror/model.go:
--------------------------------------------------------------------------------
1 | package mirror
2 |
3 | import "github.com/ibuilding-x/driver-box/driverbox/plugin"
4 |
5 | type EncodeModel struct {
6 | deviceId string
7 | points []plugin.PointData
8 | mode plugin.EncodeMode
9 | }
10 |
11 | // Device 原始设备
12 | type Device struct {
13 | deviceId string
14 | pointName string
15 | }
16 |
--------------------------------------------------------------------------------
/exports/basic/export.go:
--------------------------------------------------------------------------------
1 | package basic
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox"
5 | "github.com/ibuilding-x/driver-box/internal/export/basic"
6 | )
7 |
8 | // LoadBasicExport 加载基础Export插件
9 | // 功能:
10 | //
11 | // 创建并加载basic.NewExport()实例
12 | func LoadExport() {
13 | driverbox.Exports.LoadExport(basic.NewExport())
14 | }
15 |
--------------------------------------------------------------------------------
/exports/gateway/export.go:
--------------------------------------------------------------------------------
1 | package gateway
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox"
5 | "github.com/ibuilding-x/driver-box/internal/export/gwexport"
6 | )
7 |
8 | // LoadGatewayExport 加载网关Export插件
9 | // 功能:
10 | //
11 | // 创建并加载gwexport.New()实例
12 | func LoadExport() {
13 | driverbox.Exports.LoadExport(gwexport.New())
14 | }
15 |
--------------------------------------------------------------------------------
/res/library/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "driver": [
3 | {
4 | "key": "test_1",
5 | "protocol": [
6 | "modbus",
7 | "bacnet"
8 | ],
9 | "moId": [
10 | "aaaaa"
11 | ],
12 | "tags": [
13 | "xx",
14 | "xxxx"
15 | ]
16 | }
17 | ],
18 | "model": {
19 | },
20 | "protocol": []
21 | }
--------------------------------------------------------------------------------
/exports/mirror/export.go:
--------------------------------------------------------------------------------
1 | package mirror
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox"
5 | "github.com/ibuilding-x/driver-box/internal/export/mirror"
6 | )
7 |
8 | // LoadMirrorExport 加载镜像设备Export插件
9 | // 功能:
10 | //
11 | // 创建并加载mirror.NewExport()实例
12 | func LoadExport() {
13 | driverbox.Exports.LoadExport(mirror.NewExport())
14 | }
15 |
--------------------------------------------------------------------------------
/driverbox/helper/logger.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/logger"
5 | "go.uber.org/zap"
6 | )
7 |
8 | // Logger 日志记录器
9 | var Logger *zap.Logger
10 |
11 | // New 实例化
12 | func InitLogger(level string) (err error) {
13 | logger.InitLogger(EnvConfig.LogPath, level)
14 | Logger = logger.Logger
15 | return err
16 | }
17 |
--------------------------------------------------------------------------------
/exports/linkedge/export.go:
--------------------------------------------------------------------------------
1 | package linkedge
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox"
5 | "github.com/ibuilding-x/driver-box/internal/export/linkedge"
6 | )
7 |
8 | // LoadLinkEdgeExport 加载场景联动Export插件
9 | // 功能:
10 | //
11 | // 创建并加载linkedge.NewExport()实例
12 | func LoadExport() {
13 | driverbox.Exports.LoadExport(linkedge.NewExport())
14 | }
15 |
--------------------------------------------------------------------------------
/exports/discover/export.go:
--------------------------------------------------------------------------------
1 | package discover
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox"
5 | "github.com/ibuilding-x/driver-box/internal/export/discover"
6 | )
7 |
8 | // LoadDiscoverExport 加载设备自动发现Export插件
9 | // 功能:
10 | //
11 | // 创建并加载discover.NewExport()实例
12 | func LoadExport() {
13 | driverbox.Exports.LoadExport(discover.NewExport())
14 | }
15 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/ndpu/messagetypes.go:
--------------------------------------------------------------------------------
1 | package ndpu
2 |
3 | type NetworkMessageType uint8
4 |
5 | //go:generate stringer -type=NetworkMessageType
6 | const (
7 | WhoIsRouterToNetwork NetworkMessageType = 0x00
8 | IamRouterToNetwork NetworkMessageType = 0x01
9 | WhatIsNetworkNumber NetworkMessageType = 0x12
10 | NetworkIs NetworkMessageType = 0x13
11 | )
12 |
--------------------------------------------------------------------------------
/pages/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pages",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro build",
9 | "preview": "astro preview",
10 | "astro": "astro"
11 | },
12 | "dependencies": {
13 | "@astrojs/starlight": "^0.22.4",
14 | "astro": "^4.3.5",
15 | "sharp": "^0.32.5"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox"
5 | "github.com/ibuilding-x/driver-box/exports"
6 | "github.com/ibuilding-x/driver-box/plugins"
7 | "os"
8 | )
9 |
10 | func main() {
11 | // 设置日志级别
12 | _ = os.Setenv("LOG_LEVEL", "info")
13 | //_ = plugins.RegisterAllPlugins()
14 | plugins.RegisterAllPlugins()
15 | exports.LoadAllExports()
16 | driverbox.Start()
17 | select {}
18 | }
19 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/datalink/datalink.go:
--------------------------------------------------------------------------------
1 | package datalink
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
5 | )
6 |
7 | type DataLink interface {
8 | GetMyAddress() *btypes.Address
9 | GetBroadcastAddress() *btypes.Address
10 | Send(data []byte, npdu *btypes.NPDU, dest *btypes.Address) (int, error)
11 | Receive(data []byte) (*btypes.Address, int, error)
12 | Close() error
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Build and Release Folders
2 | bin-debug/
3 | bin-release/
4 | [Oo]bj/
5 | [Bb]in/
6 |
7 | # Other files and folders
8 | .settings/
9 |
10 | # Executables
11 | *.swf
12 | *.air
13 | *.ipa
14 | *.apk
15 |
16 | # Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
17 | # should NOT be excluded as they contain compiler settings and other important
18 | # information for Eclipse / Flash Builder.
19 |
20 | .idea
21 | vendor
22 | temp
--------------------------------------------------------------------------------
/internal/export/mirror/model.go:
--------------------------------------------------------------------------------
1 | package mirror
2 |
3 | const (
4 | PropertyKeyAutoMirrorFrom string = "autoMirrorFrom"
5 | PropertyKeyAutoMirrorTo string = "autoMirrorTo"
6 | )
7 |
8 | // 自动生成镜像的配置结构
9 | type autoMirrorConfig struct {
10 | //模型库
11 | ModelKey string `json:"modelKey"`
12 | Description string `json:"description"`
13 | //设备驱动库
14 | DriverKey string `json:"driverKey"`
15 | //点位映射关系,name和rawPoint为必要项
16 | Points []map[string]string
17 | }
18 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/const.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | // fail read or write retry count
4 | const retryCount = 1
5 |
6 | // MTSP
7 | const defaultMTSPBAUD = 38400
8 | const defaultMTSPMAC = 127
9 |
10 | // General Bacnet
11 | const defaultMaxMaster = 127
12 | const defaultMaxInfoFrames = 1
13 |
14 | // ArrayAll is used when reading/writing to a property to read/write the entire
15 | // array
16 | const ArrayAll = 0xFFFFFFFF
17 | const maxStandardBacnetType = 128
18 |
--------------------------------------------------------------------------------
/driverbox/plugin.go:
--------------------------------------------------------------------------------
1 | package driverbox
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
5 | "github.com/ibuilding-x/driver-box/internal/bootstrap"
6 | plugins0 "github.com/ibuilding-x/driver-box/internal/plugins"
7 | )
8 |
9 | // ReloadPlugins 重载所有插件
10 | func ReloadPlugins() error {
11 | return bootstrap.ReloadPlugins()
12 | }
13 |
14 | func RegisterPlugin(name string, plugin plugin.Plugin) {
15 | plugins0.Manager.Register(name, plugin)
16 | }
17 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/segmentation/segmentation.go:
--------------------------------------------------------------------------------
1 | package segmentation
2 |
3 | //BACnetSegmentation:
4 | //segmented-both:0
5 | //segmented-transmit:1
6 | //segmented-receive:2
7 | //no-segmentation: 3
8 |
9 | type SegmentedType uint8
10 |
11 | //go:generate stringer -type=SegmentedType
12 | const (
13 | SegmentedBoth SegmentedType = 0x00
14 | SegmentedTransmit SegmentedType = 0x01
15 | SegmentedReceive SegmentedType = 0x02
16 | NoSegmentation SegmentedType = 0x03
17 | )
18 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/misc.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | // From:
4 | // https://stackoverflow.com/questions/6878590/the-maximum-value-for-an-int-type-in-go
5 | const (
6 | maxUint = ^uint(0)
7 | minUint = 0
8 | // based on 2's complement structure of max int
9 | maxInt = int(maxUint >> 1)
10 | minInt = -maxInt - 1
11 | )
12 |
13 | func max(a, b int) int {
14 | if a > b {
15 | return a
16 | }
17 | return b
18 | }
19 |
20 | func min(a, b int) int {
21 | if a < b {
22 | return a
23 | }
24 | return b
25 | }
26 |
--------------------------------------------------------------------------------
/res/library/driver/test_1.lua:
--------------------------------------------------------------------------------
1 | local json = require("json")
2 |
3 | function decode(deviceId, points)
4 | local returnPoints = {}
5 | for _, point in pairs(points) do
6 | --print("value: " .. point["value"])
7 | table.insert(returnPoints, {
8 | name = point["name"],
9 | value = point["value"],
10 | })
11 | end
12 | return json.encode(returnPoints)
13 | end
14 |
15 | function encode(deviceId, rw, points)
16 | return error("this device can not be encoded")
17 | end
--------------------------------------------------------------------------------
/internal/export/discover/model.go:
--------------------------------------------------------------------------------
1 | package discover
2 |
3 | import "github.com/ibuilding-x/driver-box/driverbox/config"
4 |
5 | type DeviceDiscover struct {
6 | ModelName string `json:"modelName"` //模型名称后缀
7 | ModelKey string `json:"modelKey"` //模型Key
8 | Device config.Device `json:"device"`
9 | //模型自定义属性
10 | Model map[string]map[string]any `json:"model"`
11 | ProtocolName string `json:"protocolName"`
12 | ConnectionKey string `json:"connectionKey"`
13 | }
14 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/error.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | type TagType string
8 |
9 | const (
10 | ContextTag TagType = "context"
11 | OpeningTag TagType = "opening"
12 | ClosingTag TagType = "closing"
13 | )
14 |
15 | // ErrorWrongTagType is given when a certain tag type is expected but not given when encoding/decoding
16 | type ErrorWrongTagType struct {
17 | Type TagType
18 | }
19 |
20 | func (e *ErrorWrongTagType) Error() string {
21 | return fmt.Sprintf("Tag should be a %s tag", e.Type)
22 | }
23 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/types.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import "fmt"
4 |
5 | type ErrorIncorrectTag struct {
6 | Expected uint8
7 | Given uint8
8 | }
9 |
10 | func (e *ErrorIncorrectTag) Error() string {
11 | return fmt.Sprintf("Incorrect tag %d, expected %d.", e.Given, e.Expected)
12 | }
13 |
14 | type tagInfo struct {
15 | // Tag id. Typically sequential, except when it is not...
16 | ID uint8
17 | Context bool
18 | // Either has a value or length of the next value
19 | Value uint32
20 | Opening bool
21 | Closing bool
22 | }
23 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/core/buffer.go:
--------------------------------------------------------------------------------
1 | package dlt645
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type pool struct {
8 | pl *sync.Pool
9 | }
10 |
11 | func newPool(size int) *pool {
12 | return &pool{
13 | &sync.Pool{
14 | New: func() interface{} {
15 | return &protocolFrame{make([]byte, 0, size)}
16 | },
17 | },
18 | }
19 | }
20 |
21 | func (sf *pool) get() *protocolFrame {
22 | v := sf.pl.Get().(*protocolFrame)
23 | v.adu = v.adu[:0]
24 | return v
25 | }
26 |
27 | func (sf *pool) put(buffer *protocolFrame) {
28 | sf.pl.Put(buffer)
29 | }
30 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/helpers/print/console.go:
--------------------------------------------------------------------------------
1 | package pprint
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 | )
8 |
9 | func Print(i interface{}) {
10 | fmt.Printf("%+v\n", i)
11 | return
12 | }
13 |
14 | func Log(i interface{}) string {
15 |
16 | return fmt.Sprintf("%+v\n", i)
17 | }
18 | func PrintJOSN(x interface{}) {
19 | ioWriter := os.Stdout
20 | w := json.NewEncoder(ioWriter)
21 | w.SetIndent("", " ")
22 | w.Encode(x)
23 | }
24 |
25 | func ToJOSN(x interface{}) string {
26 | w, _ := json.Marshal(x)
27 | return string(w)
28 | }
29 |
--------------------------------------------------------------------------------
/driverbox/helper/crontab/crontab_test.go:
--------------------------------------------------------------------------------
1 | package crontab
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestCrontab_AddFunc(t *testing.T) {
10 | cron := Instance()
11 | future, err := cron.AddFunc("3s", func() {
12 | fmt.Println("3s")
13 | })
14 |
15 | cron.AddFunc("2s", func() {
16 | fmt.Println("2s")
17 | })
18 |
19 | if err != nil {
20 | t.Error(err)
21 | return
22 | }
23 | time.Sleep(10 * time.Second)
24 | fmt.Println("disable cron")
25 | future.Disable()
26 | time.Sleep(5 * time.Second)
27 | fmt.Println("finish cron")
28 | }
29 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/const.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | const MaxInstance = 0x3FFFFF
4 | const InstanceBits = 22
5 | const MaxPropertyID = 4194303
6 |
7 | const MaxAPDUOverIP = 1476
8 | const MaxAPDU = MaxAPDUOverIP
9 |
10 | const initialTagPos = 0
11 |
12 | const (
13 | size8 = 1
14 | size16 = 2
15 | size24 = 3
16 | size32 = 4
17 | )
18 |
19 | const (
20 | flag16bit uint8 = 254
21 | flag32bit uint8 = 255
22 | )
23 |
24 | // ArrayAll is an argument typically passed during a read to signify where to
25 | // read
26 | const ArrayAll uint32 = ^uint32(0)
27 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/bvlc.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
5 | )
6 |
7 | // Bacnet Virtual Layer Control
8 |
9 | func (e *Encoder) BVLC(b btypes.BVLC) error {
10 | // Set packet type
11 | e.write(b.Type)
12 | e.write(b.Function)
13 | e.write(b.Length)
14 | e.write(b.Data)
15 | return e.Error()
16 | }
17 |
18 | func (d *Decoder) BVLC(b *btypes.BVLC) error {
19 | d.decode(&b.Type)
20 | d.decode(&b.Function)
21 | d.decode(&b.Length)
22 | d.decode(&b.Data)
23 | return d.Error()
24 | }
25 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/validate.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import (
4 | "fmt"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
6 | )
7 |
8 | func isValidObjectType(idType btypes.ObjectType) error {
9 | if idType > MaxObject {
10 | return fmt.Errorf("Object btypes is %d which must be less then %d", idType, MaxObject)
11 | }
12 | return nil
13 | }
14 |
15 | func isValidPropertyType(propType uint32) error {
16 | if propType > MaxPropertyID {
17 | return fmt.Errorf("Object btypes is %d which must be less then %d", propType, MaxPropertyID)
18 | }
19 | return nil
20 | }
21 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/network/whois.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
6 | )
7 |
8 | func (device *Device) Whois(options *bacnet.WhoIsOpts) ([]btypes.Device, error) {
9 | // go device.network.ClientRun()
10 | resp, err := device.network.WhoIs(options)
11 | return resp, err
12 | }
13 |
14 | func (net *Network) Whois(options *bacnet.WhoIsOpts) ([]btypes.Device, error) {
15 | // go net.NetworkRun()
16 | resp, err := net.Client.WhoIs(options)
17 | return resp, err
18 | }
19 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/writemultiple.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
4 |
5 | // WriteMultiProperty encodes a writes property request
6 | func (e *Encoder) WriteMultiProperty(invokeID uint8, data btypes.MultiplePropertyData) error {
7 | a := btypes.APDU{
8 | DataType: btypes.ConfirmedServiceRequest,
9 | Service: btypes.ServiceConfirmedWritePropMultiple,
10 | MaxSegs: 0,
11 | MaxApdu: MaxAPDU,
12 | InvokeId: invokeID,
13 | }
14 | e.APDU(a)
15 |
16 | err := e.objects(data.Objects, true)
17 | if err != nil {
18 | return err
19 | }
20 |
21 | return e.Error()
22 | }
23 |
--------------------------------------------------------------------------------
/res/driver/mirror/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "deviceModels": [
3 | {
4 | "name": "swtich",
5 | "description": "开关",
6 | "devicePoints": [
7 | {
8 | "description": "开关",
9 | "name": "onOff",
10 | "readWrite": "RW",
11 | "reportMode": "change",
12 | "valueType": "int",
13 | "rawDevice": "swtich-1",
14 | "rawPoint": "onOff"
15 | }
16 | ],
17 | "devices": [
18 | {
19 | "id": "mirror-swtich-3",
20 | "description": "1号开关",
21 | "ttl": "5m"
22 | }
23 | ]
24 | }
25 | ],
26 | "connections": {
27 | },
28 | "protocolName": "mirror"
29 | }
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/readmultiple.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
5 | )
6 |
7 | func (e *Encoder) ReadMultipleProperty(invokeID uint8, data btypes.MultiplePropertyData) error {
8 | a := btypes.APDU{
9 | DataType: btypes.ConfirmedServiceRequest,
10 | Service: btypes.ServiceConfirmedReadPropMultiple,
11 | MaxSegs: 0,
12 | MaxApdu: MaxAPDU,
13 | InvokeId: invokeID,
14 | SegmentedMessage: false,
15 | }
16 | e.APDU(a)
17 | err := e.objects(data.Objects, false)
18 | if err != nil {
19 | return err
20 | }
21 |
22 | return e.Error()
23 | }
24 |
--------------------------------------------------------------------------------
/pages/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/exports/export.go:
--------------------------------------------------------------------------------
1 | package exports
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/exports/basic"
5 | "github.com/ibuilding-x/driver-box/exports/discover"
6 | "github.com/ibuilding-x/driver-box/exports/gateway"
7 | "github.com/ibuilding-x/driver-box/exports/linkedge"
8 | "github.com/ibuilding-x/driver-box/exports/mirror"
9 | "github.com/ibuilding-x/driver-box/exports/ui"
10 | )
11 |
12 | // LoadAllExports 加载driver-box框架内置的所有Export插件
13 | // 功能:
14 | //
15 | // 依次调用各个内置Export的加载方法,包括基础Export、场景联动Export等
16 | func LoadAllExports() {
17 | basic.LoadExport()
18 | linkedge.LoadExport()
19 | mirror.LoadExport()
20 | ui.LoadExport()
21 | discover.LoadExport()
22 | gateway.LoadExport()
23 | }
24 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/core/crc_test.go:
--------------------------------------------------------------------------------
1 | package dlt645
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func Test_crc16(t *testing.T) {
8 | type args struct {
9 | bs []byte
10 | }
11 | tests := []struct {
12 | name string
13 | args args
14 | want uint16
15 | }{
16 | {"crc16 ", args{[]byte{0x01, 0x02, 0x03, 0x04, 0x05}}, 0xbb2a},
17 | }
18 | for _, tt := range tests {
19 | t.Run(tt.name, func(t *testing.T) {
20 | if got := crc16(tt.args.bs); got != tt.want {
21 | t.Errorf("crc16() = %v, want %v", got, tt.want)
22 | }
23 | })
24 | }
25 | }
26 |
27 | func Benchmark_crc16(b *testing.B) {
28 | for i := 0; i < b.N; i++ {
29 | _ = crc16([]byte{0x01, 0x02, 0x03, 0x04, 0x05})
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/internal/export/export.go:
--------------------------------------------------------------------------------
1 | package export
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/export"
5 | "github.com/ibuilding-x/driver-box/internal/logger"
6 | "go.uber.org/zap"
7 | )
8 |
9 | var Exports []export.Export
10 |
11 | // 触发事件
12 | func TriggerEvents(eventCode string, key string, value interface{}) {
13 | for _, export0 := range Exports {
14 | if !export0.IsReady() {
15 | logger.Logger.Debug("export not ready")
16 | continue
17 | }
18 | err := export0.OnEvent(eventCode, key, value)
19 | if err != nil {
20 | logger.Logger.Error("trigger event error", zap.String("eventCode", eventCode), zap.String("key", key), zap.Any("value", value), zap.Error(err))
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/iam_test.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | import (
4 | "fmt"
5 | pprint "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/helpers/print"
6 | "go/build"
7 | "os"
8 | "testing"
9 | )
10 |
11 | var iface = "enp0s31f6"
12 |
13 | func TestIam(t *testing.T) {
14 |
15 | gopath := os.Getenv("GOPATH")
16 | if gopath == "" {
17 | gopath = build.Default.GOPATH
18 | }
19 | fmt.Println(gopath)
20 |
21 | cb := &ClientBuilder{
22 | Interface: iface,
23 | }
24 | c, _ := NewClient(cb)
25 | defer c.Close()
26 | go c.ClientRun()
27 |
28 | //resp := c.WhatIsNetworkNumber()
29 |
30 | resp := c.WhoIsRouterToNetwork()
31 | fmt.Println("WhoIsRouterToNetwork")
32 | pprint.Print(resp)
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/internal/logger/chan_writer.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | var ChanWriter = &ChannelWriter{
8 | clients: &sync.Map{},
9 | }
10 |
11 | type ChannelWriter struct {
12 | clients *sync.Map
13 | }
14 |
15 | func (writer *ChannelWriter) Add(loggerChan chan []byte) {
16 | writer.clients.Store(loggerChan, true)
17 | }
18 | func (writer *ChannelWriter) Write(p []byte) (n int, err error) {
19 | writer.clients.Range(func(key, value interface{}) bool {
20 | loggerChan := key.(chan []byte)
21 | if len(loggerChan) < 100 {
22 | loggerChan <- p
23 | }
24 | return true
25 | })
26 | return 0, nil
27 | }
28 |
29 | func (writer ChannelWriter) Remove(loggerChan chan []byte) {
30 | writer.clients.Delete(loggerChan)
31 | }
32 |
--------------------------------------------------------------------------------
/res/driver/http_server/converter.lua:
--------------------------------------------------------------------------------
1 | local json = require("json")
2 |
3 | -- decode 请求数据解码
4 | -- curl -X POST -H "Content-Type: application/json" -d '{"id":"swtich-2","onOff":1}' http://127.0.0.1:8888/report
5 | function decode(raw)
6 | local data = json.decode(raw)
7 | local body= json.decode(data.body)
8 | if data.method == "POST" and data.path == "/report" then
9 | local devices = {
10 | {
11 | ["id"] = body["id"], -- 设备ID
12 | ["values"] = {
13 | { ["name"] = "onOff", ["value"] = body["onOff"] }, -- 点位解析
14 | }
15 | }
16 | }
17 | return json.encode(devices)
18 | else
19 | print("request error")
20 | return "[]"
21 | end
22 | end
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/helpers/validation/validation_test.go:
--------------------------------------------------------------------------------
1 | package validation
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | )
7 |
8 | func TestPort(t *testing.T) {
9 |
10 | port := 8080
11 | ok := ValidPort(port)
12 | fmt.Println("port:", port, "is ok", ok)
13 |
14 | port = -1
15 | ok = ValidPort(port)
16 | fmt.Println("port:", port, "is ok", ok)
17 |
18 | port = 0
19 | ok = ValidPort(port)
20 | fmt.Println("port:", port, "is ok", ok)
21 |
22 | port = 47808
23 | ok = ValidPort(port)
24 | fmt.Println("port:", port, "is ok", ok)
25 |
26 | port = 24
27 | ok = ValidCIDR("192.168.15.1", port)
28 | fmt.Println("ValidCIDR:", port, "is ok", ok)
29 |
30 | port = 2000
31 | ok = ValidCIDR("192.168.15.1", port)
32 | fmt.Println("ValidCIDR:", port, "is ok", ok)
33 | }
34 |
--------------------------------------------------------------------------------
/driverbox/library/mirror_tpl.go:
--------------------------------------------------------------------------------
1 | package library
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/ibuilding-x/driver-box/driverbox/common"
7 | "github.com/ibuilding-x/driver-box/driverbox/config"
8 | "path"
9 | )
10 |
11 | type MirrorTemplate struct {
12 | }
13 |
14 | // 加载指定key的驱动
15 | func (device *MirrorTemplate) LoadLibrary(key string) (map[string]interface{}, error) {
16 | filePath := path.Join(config.ResourcePath, baseDir, string(mirrorTemplate), key+".json")
17 | if !common.FileExists(filePath) {
18 | return nil, fmt.Errorf("mirror template not found: %s", key)
19 | }
20 | //读取filePath中的文件内容
21 | bytes, e := common.ReadFileBytes(filePath)
22 | if e != nil {
23 | return nil, e
24 | }
25 | var result map[string]interface{}
26 | e = json.Unmarshal(bytes, &result)
27 | return result, e
28 | }
29 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/iam.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/encoding"
6 | )
7 |
8 | /*
9 | not working
10 | */
11 |
12 | func (c *client) IAm(dest btypes.Address, iam btypes.IAm) error {
13 | npdu := &btypes.NPDU{
14 | Version: btypes.ProtocolVersion,
15 | Destination: &dest,
16 | //IsNetworkLayerMessage: true,
17 | //NetworkLayerMessageType: 0x12,
18 | //Source: c.dataLink.GetMyAddress(),
19 | ExpectingReply: false,
20 | Priority: btypes.Normal,
21 | HopCount: btypes.DefaultHopCount,
22 | }
23 | enc := encoding.NewEncoder()
24 | enc.NPDU(npdu)
25 | enc.IAm(iam)
26 | _, err := c.Send(dest, npdu, enc.Bytes(), nil)
27 | return err
28 | }
29 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/npdu.go:
--------------------------------------------------------------------------------
1 | package btypes
2 |
3 | import "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes/ndpu"
4 |
5 | type NPDUPriority byte
6 |
7 | const ProtocolVersion uint8 = 1
8 | const DefaultHopCount uint8 = 255
9 |
10 | const (
11 | LifeSafety NPDUPriority = 3
12 | CriticalEquipment NPDUPriority = 2
13 | Urgent NPDUPriority = 1
14 | Normal NPDUPriority = 0
15 | )
16 |
17 | type NPDU struct {
18 | Version uint8
19 |
20 | // Destination (optional)
21 | Destination *Address
22 |
23 | // Source (optional)
24 | Source *Address
25 |
26 | VendorId uint16
27 |
28 | IsNetworkLayerMessage bool
29 | NetworkLayerMessageType ndpu.NetworkMessageType
30 | ExpectingReply bool
31 | Priority NPDUPriority
32 | HopCount uint8
33 | }
34 |
--------------------------------------------------------------------------------
/driverbox/export/linkedge/trigger.go:
--------------------------------------------------------------------------------
1 | package linkedge
2 |
3 | // TriggerType 触发器类型
4 | type TriggerType string
5 |
6 | const (
7 | // TriggerTypeSchedule 事件表触发器
8 | TriggerTypeSchedule TriggerType = "schedule"
9 | // TriggerTypeDevicePoint 设备点位触发器
10 | TriggerTypeDevicePoint TriggerType = "devicePoint"
11 | // TriggerTypeDeviceEvent 设备事件触发器(暂未使用)
12 | TriggerTypeDeviceEvent TriggerType = "deviceEvent"
13 | )
14 |
15 | // Trigger 触发器
16 | type Trigger struct {
17 | Type TriggerType `json:"type"`
18 | ScheduleTrigger
19 | DevicePointTrigger
20 | }
21 |
22 | // ScheduleTrigger 定时触发器
23 | type ScheduleTrigger struct {
24 | Cron string `json:"cron"`
25 | }
26 |
27 | // DevicePointTrigger 设备点位触发器
28 | type DevicePointTrigger struct {
29 | DevicePointCondition
30 | }
31 |
32 | // DeviceEventTrigger 设备事件触发器
33 | // 提示:暂未使用
34 | type DeviceEventTrigger struct {
35 | }
36 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/utsm/main_test.go:
--------------------------------------------------------------------------------
1 | package utsm
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func sub(t *testing.T, m *Manager, start, end int) {
10 | b, err := m.Subscribe(start, end)
11 | if err != nil {
12 | t.Fatal(err)
13 | }
14 |
15 | t.Logf("[%d, %d] %v", start, end, b)
16 | }
17 |
18 | func publisher(t *testing.T, m *Manager) {
19 | for i := 0; i < 5; i++ {
20 | go m.Publish(20, fmt.Sprintf("HI!%d", i))
21 | time.Sleep(time.Duration(100) * time.Millisecond)
22 | }
23 | }
24 | func TestUTSM(t *testing.T) {
25 | opts := []ManagerOption{
26 | DefaultSubscriberTimeout(time.Duration(2) * time.Second),
27 | DefaultSubscriberLastReceivedTimeout(time.Duration(300) * time.Millisecond),
28 | }
29 | m := NewManager(opts...)
30 |
31 | go publisher(t, m)
32 | go sub(t, m, 9, 20)
33 | go sub(t, m, 0, 2)
34 | sub(t, m, 10, 30)
35 | }
36 |
--------------------------------------------------------------------------------
/driverbox/export/linkedge/config.go:
--------------------------------------------------------------------------------
1 | package linkedge
2 |
3 | import "time"
4 |
5 | type Config struct {
6 | // ID 场景ID
7 | ID string `json:"id,omitempty"`
8 | // Enable 是否可用
9 | Enable bool `json:"enable"`
10 | // Name 场景名称
11 | Name string `json:"name"`
12 | // Tags 场景标签
13 | Tags []string `json:"tags"`
14 | // Description 场景描述
15 | Description string `json:"description"`
16 | // SilentPeriod 静默期,单位:秒
17 | SilentPeriod int64 `json:"silentPeriod"`
18 | // Trigger 触发器
19 | Trigger []Trigger `json:"trigger"`
20 | // Condition 执行条件
21 | Condition []Condition `json:"condition"`
22 | // Action 执行动作
23 | Action []Action `json:"action"`
24 | // ExecuteTime 最后执行时间
25 | ExecuteTime time.Time
26 | }
27 |
28 | func (c *Config) ExistTag(tag string) bool {
29 | for i, _ := range c.Tags {
30 | if tag == c.Tags[i] {
31 | return true
32 | }
33 | }
34 |
35 | return false
36 | }
37 |
--------------------------------------------------------------------------------
/driverbox/helper/helper.go:
--------------------------------------------------------------------------------
1 | // 核心工具助手文件
2 |
3 | package helper
4 |
5 | import (
6 | "encoding/json"
7 |
8 | "github.com/ibuilding-x/driver-box/driverbox/config"
9 | "github.com/ibuilding-x/driver-box/driverbox/helper/crontab"
10 | "github.com/ibuilding-x/driver-box/driverbox/pkg/shadow"
11 | "github.com/ibuilding-x/driver-box/internal/core/cache"
12 |
13 | "sync"
14 | )
15 |
16 | var DeviceShadow shadow.DeviceShadow // 本地设备影子
17 | // CoreCache 核心缓存
18 | var CoreCache cache.CoreCache
19 | var PluginCacheMap = &sync.Map{} // 插件通用缓存
20 |
21 | var Crontab crontab.Crontab // 全局定时任务实例
22 |
23 | var EnvConfig config.EnvConfig
24 |
25 | // Map2Struct map 转 struct,用于解析连接器配置
26 | // m:map[string]interface
27 | // v:&struct{}
28 | func Map2Struct(m interface{}, v interface{}) error {
29 | b, err := json.Marshal(m)
30 | if err != nil {
31 | return err
32 | }
33 | return json.Unmarshal(b, v)
34 | }
35 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/core/crc.go:
--------------------------------------------------------------------------------
1 | package dlt645
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | // Cyclical Redundancy Checking
8 | type crc struct {
9 | once sync.Once
10 | table []uint16
11 | }
12 |
13 | var crcTb crc
14 |
15 | func crc16(bs []byte) uint16 {
16 | crcTb.once.Do(crcTb.initTable)
17 |
18 | val := uint16(0xFFFF)
19 | for _, v := range bs {
20 | val = (val >> 8) ^ crcTb.table[(val^uint16(v))&0x00FF]
21 | }
22 | return val
23 | }
24 |
25 | // initTable 初始化表
26 | func (c *crc) initTable() {
27 | crcPoly16 := uint16(0xa001)
28 | c.table = make([]uint16, 256)
29 |
30 | for i := uint16(0); i < 256; i++ {
31 | crc := uint16(0)
32 | b := i
33 |
34 | for j := uint16(0); j < 8; j++ {
35 | if ((crc ^ b) & 0x0001) > 0 {
36 | crc = (crc >> 1) ^ crcPoly16
37 | } else {
38 | crc = crc >> 1
39 | }
40 | b = b >> 1
41 | }
42 | c.table[i] = crc
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/marshal_test.go:
--------------------------------------------------------------------------------
1 | package btypes
2 |
3 | import (
4 | "encoding/json"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | // TestMarshal tests encoding and decoding of the objectmap type. There is
10 | // custom logic in it so we want to make sure it works.
11 | func TestMarshal(t *testing.T) {
12 | test := ObjectMap{
13 | AnalogInput: make(map[ObjectInstance]Object),
14 | BinaryOutput: make(map[ObjectInstance]Object),
15 | }
16 | test[AnalogInput][0] = Object{Name: "Pizza Sensor"}
17 | test[BinaryOutput][4] = Object{Name: "Should I Eat Pizza Sensor"}
18 | b, err := json.Marshal(test)
19 | if err != nil {
20 | t.Fatal(err)
21 | }
22 |
23 | out := make(ObjectMap, 0)
24 | err = json.Unmarshal(b, &out)
25 | if err != nil {
26 | t.Fatal(err)
27 | }
28 |
29 | if !reflect.DeepEqual(test, out) {
30 | t.Fatal("Encoding/decoding Object map is not equal")
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/driverbox/pkg/mbserver/internal/storage.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | type Device struct {
4 | Start uint16 // 寄存器起始地址
5 | End uint16 // 寄存器结束地址
6 | Model uint16 // 模型索引
7 | }
8 |
9 | type Model struct {
10 | Id string
11 | Name string // 可选
12 | Quantity uint16 // 寄存器数量
13 | PropertyIndexStart uint16 // 属性起始索引
14 | PropertyIndexEnd uint16 // 属性结束索引
15 | Property map[string]uint16 // 属性名称与索引映射
16 | }
17 |
18 | type Property struct {
19 | Description string `json:"description"` // 可选
20 | RelativeStartAddress uint16 // 属性相对起始地址
21 | Quantity uint16 // 寄存器数量
22 | Name string // 属性名称
23 | ValueType int // 属性值类型
24 | Access int // 属性访问权限
25 | }
26 |
27 | type RegisterUnit struct {
28 | Id string // 设备 ID
29 | Property uint16 // 属性索引
30 | }
31 |
--------------------------------------------------------------------------------
/internal/export/gwexport/gwexport.go:
--------------------------------------------------------------------------------
1 | package gwexport
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/export"
5 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
6 | )
7 |
8 | type gatewayExport struct {
9 | wss *websocketService
10 | }
11 |
12 | // Init 初始化
13 | func (g *gatewayExport) Init() error {
14 | g.wss.Start()
15 | return nil
16 | }
17 |
18 | func (g *gatewayExport) Destroy() error {
19 | return nil
20 | }
21 |
22 | // ExportTo 接收驱动数据
23 | func (g *gatewayExport) ExportTo(deviceData plugin.DeviceData) {
24 | g.wss.sendDeviceData(deviceData)
25 | }
26 |
27 | // OnEvent 接收事件数据
28 | func (g *gatewayExport) OnEvent(eventCode string, key string, eventValue interface{}) error {
29 | // 暂时不处理任何事件
30 | return nil
31 | }
32 |
33 | func (g *gatewayExport) IsReady() bool {
34 | return true
35 | }
36 |
37 | func New() export.Export {
38 | return &gatewayExport{
39 | wss: &websocketService{},
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/segmentation/segmentedtype_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type=SegmentedType"; DO NOT EDIT.
2 |
3 | package segmentation
4 |
5 | import "strconv"
6 |
7 | func _() {
8 | // An "invalid array index" compiler error signifies that the constant values have changed.
9 | // Re-run the stringer command to generate them again.
10 | var x [1]struct{}
11 | _ = x[SegmentedBoth-0]
12 | _ = x[SegmentedTransmit-1]
13 | _ = x[SegmentedReceive-2]
14 | _ = x[NoSegmentation-3]
15 | }
16 |
17 | const _SegmentedType_name = "SegmentedBothSegmentedTransmitSegmentedReceiveNoSegmentation"
18 |
19 | var _SegmentedType_index = [...]uint8{0, 13, 30, 46, 60}
20 |
21 | func (i SegmentedType) String() string {
22 | if i >= SegmentedType(len(_SegmentedType_index)-1) {
23 | return "SegmentedType(" + strconv.FormatInt(int64(i), 10) + ")"
24 | }
25 | return _SegmentedType_name[_SegmentedType_index[i]:_SegmentedType_index[i+1]]
26 | }
27 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/core/dltcon/option.go:
--------------------------------------------------------------------------------
1 | package dltcon
2 |
3 | // Option 可选项
4 | type Option func(client *Client)
5 |
6 | // WithReadyQueueSize 就绪队列长度
7 | func WithReadyQueueSize(size int) Option {
8 | return func(client *Client) {
9 | client.readyQueueSize = size
10 | }
11 | }
12 |
13 | // WitchHandler 配置handler
14 | func WitchHandler(h Handler) Option {
15 | return func(client *Client) {
16 | if h != nil {
17 | client.handler = h
18 | }
19 | }
20 | }
21 |
22 | // WitchRetryRandValue 单位ms
23 | // 默认随机值上限,它影响当超时请求入ready队列时,
24 | // 当队列满,会启动一个随机时间rand.Intn(v)*1ms 延迟入队
25 | // 用于需要重试的延迟重试时间
26 | func WitchRetryRandValue(v int) Option {
27 | return func(client *Client) {
28 | if v > 0 {
29 | client.randValue = v
30 | }
31 | }
32 | }
33 |
34 | // WitchPanicHandle 发生panic回调,主要用于调试
35 | func WitchPanicHandle(f func(interface{})) Option {
36 | return func(client *Client) {
37 | if f != nil {
38 | client.panicHandle = f
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/res/driver/websocket/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "deviceModels": [
3 | {
4 | "name": "swtich",
5 | "description": "开关",
6 | "devicePoints": [
7 | {
8 | "description": "开关",
9 | "name": "onOff",
10 | "readWrite": "R",
11 | "reportMode": "change",
12 | "valueType": "int"
13 | }
14 | ],
15 | "devices": [
16 | {
17 | "id": "ws-swtich-1",
18 | "description": "1号开关",
19 | "ttl": "5m",
20 | "connectionKey": "8081"
21 | },
22 | {
23 | "id": "ws-swtich-2",
24 | "description": "2号开关",
25 | "ttl": "5m",
26 | "connectionKey": "8081"
27 | }
28 | ]
29 | }
30 | ],
31 | "connections": {
32 | "8081": {
33 | "host": "127.0.0.1",
34 | "port": 8081,
35 | "pattern": "/ws",
36 | "driverKey": "ws_demo"
37 | }
38 | },
39 | "protocolName": "websocket"
40 | }
--------------------------------------------------------------------------------
/res/library/driver/test_2.lua:
--------------------------------------------------------------------------------
1 | local json = require("json")
2 |
3 | -- 格式化数字,最多保留两位小数
4 | function format_number(num)
5 | v = math.floor(num)
6 | if num == v then
7 | return v
8 | end
9 | local formatted = string.format("%.2f", num)
10 | formatted = string.gsub(formatted, "%.?0+$", "")
11 | return formatted
12 | end
13 |
14 | function decode(deviceId, points)
15 | local returnPoints = {}
16 | for _, point in pairs(points) do
17 | table.insert(returnPoints, {
18 | name = point["name"],
19 | value = point["value"],
20 | })
21 | end
22 | return json.encode(returnPoints)
23 | end
24 |
25 | function encode(deviceId, rw, points)
26 | local returnPoints = {}
27 | for _, point in pairs(points) do
28 | table.insert(returnPoints, {
29 | name = point["name"],
30 | value = point["value"],
31 | })
32 | end
33 | return json.encode(returnPoints)
34 | end
--------------------------------------------------------------------------------
/plugins/plugin_all.go:
--------------------------------------------------------------------------------
1 | package plugins
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/plugins/bacnet"
5 | "github.com/ibuilding-x/driver-box/plugins/dlt645"
6 | "github.com/ibuilding-x/driver-box/plugins/gateway"
7 | "github.com/ibuilding-x/driver-box/plugins/httpclient"
8 | "github.com/ibuilding-x/driver-box/plugins/httpserver"
9 | "github.com/ibuilding-x/driver-box/plugins/mirror"
10 | "github.com/ibuilding-x/driver-box/plugins/modbus"
11 | "github.com/ibuilding-x/driver-box/plugins/mqtt"
12 | "github.com/ibuilding-x/driver-box/plugins/tcpserver"
13 | "github.com/ibuilding-x/driver-box/plugins/websocket"
14 | )
15 |
16 | func RegisterAllPlugins() {
17 | modbus.RegisterPlugin()
18 | bacnet.RegisterPlugin()
19 | httpserver.RegisterPlugin()
20 | httpclient.RegisterPlugin()
21 | websocket.RegisterPlugin()
22 | tcpserver.RegisterPlugin()
23 | mqtt.RegisterPlugin()
24 | mirror.RegisterPlugin()
25 | dlt645.RegisterPlugin()
26 | gateway.RegisterPlugin()
27 | }
28 |
--------------------------------------------------------------------------------
/res/driver/http_server/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "deviceModels": [
3 | {
4 | "name": "swtich",
5 | "description": "开关",
6 | "devicePoints": [
7 | {
8 | "description": "开关",
9 | "name": "onOff",
10 | "readWrite": "R",
11 | "reportMode": "change",
12 | "valueType": "int"
13 | }
14 | ],
15 | "devices": [
16 | {
17 | "id": "swtich-1",
18 | "description": "1号开关",
19 | "ttl": "5m",
20 | "connectionKey": "8888"
21 | },
22 | {
23 | "id": "swtich-2",
24 | "description": "2号开关",
25 | "ttl": "5m",
26 | "connectionKey": "8889"
27 | }
28 | ]
29 | }
30 | ],
31 | "connections": {
32 | "8888": {
33 | "host": "127.0.0.1",
34 | "port": 8888
35 | },
36 | "8889": {
37 | "host": "",
38 | "port": 8889
39 | }
40 | },
41 | "protocolName": "http_server"
42 | }
--------------------------------------------------------------------------------
/res/library/protocol/ws_demo.lua:
--------------------------------------------------------------------------------
1 | local json = require("json")
2 |
3 | -- ws示例:{"id":"ws-swtich-2","points":[{"name":"onOff","value":"2"}]}
4 | function decode(raw)
5 | local data = json.decode(raw)
6 | --for k, v in pairs(data) do
7 | -- print(k, v)
8 | --end
9 | if data["event"] ~= "read" then
10 | return "[]"
11 | end
12 | -- 打印 data
13 | print("data:" .. data["payload"])
14 | local payload = json.decode(data["payload"])
15 |
16 | local device = {
17 | ["id"] = payload["id"],
18 | ["values"] = {
19 | },
20 | }
21 | for _, point in pairs(payload["points"]) do
22 | --print("value: " .. point["value"])
23 | table.insert(device["values"], {
24 | ["name"] = point["name"],
25 | ["value"] = point["value"],
26 | })
27 | end
28 |
29 | return json.encode({ device })
30 | end
31 |
32 | function encode(deviceId, rw, points)
33 | return error("this device can not be encoded")
34 | end
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/helpers/store/init.go:
--------------------------------------------------------------------------------
1 | package store
2 |
3 | import (
4 | "github.com/patrickmn/go-cache"
5 | "time"
6 | )
7 |
8 | //var store *cache.Cache
9 |
10 | type Handler struct {
11 | Store *cache.Cache
12 | }
13 |
14 | // Init init store
15 | func Init() *Handler {
16 | newStore := cache.New(cache.NoExpiration, cache.DefaultExpiration)
17 | store := &Handler{Store: newStore}
18 | return store
19 | }
20 |
21 | // Get an item from the store. Returns the item or nil, and a bool indicating
22 | // whether the key was found.
23 | func (l *Handler) Get(key string) (interface{}, bool) {
24 | value, found := l.Store.Get(key)
25 | return value, found
26 | }
27 |
28 | // Set an item to the store, replacing any existing item. If the duration is 0
29 | // (DefaultExpiration), the store's default expiration time is used. If it is -1
30 | // (NoExpiration), the item never expires.
31 | func (l *Handler) Set(key string, value interface{}, d time.Duration) {
32 | l.Store.Set(key, value, d)
33 | }
34 |
--------------------------------------------------------------------------------
/pages/src/content/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: driver-box
3 | description: 一款嵌入式边缘平台
4 | template: splash
5 | hero:
6 | tagline: a lightweight embedded edge engine
7 | image:
8 | file: ../../assets/houston.webp
9 | actions:
10 | - text: 快速操作
11 | link: ./guides/about/
12 | icon: right-arrow
13 | variant: primary
14 | - text: 前往 GitHub 仓库
15 | link: https://github.com/iBUILDING-X/driver-box
16 | icon: external
17 | ---
18 |
19 | import { Card, CardGrid } from '@astrojs/starlight/components';
20 |
21 | ## 产品特色
22 |
23 |
24 |
25 | 通过 JSON 描述文件,实现 配置化 设备接入。
26 |
27 |
28 | 任意通讯设备(Modbus\Bacnet\MQTT\HTTP\...),接入之际便统一适配成标准化物模型。
29 |
30 |
31 | 最终编译二进制程序仅需十几MB;已知最小运行环境为 128MB 的低规格网关设备。
32 |
33 |
34 | 驱动层插件化设计,可无限适配各类设备协议。应用层统一接口设计覆盖边缘全场景:边缘计算、场景联动、边缘AI
35 |
36 |
37 |
--------------------------------------------------------------------------------
/driverbox/plugin/plugin.go:
--------------------------------------------------------------------------------
1 | // 插件接口
2 |
3 | package plugin
4 |
5 | import (
6 | "encoding/json"
7 | "github.com/ibuilding-x/driver-box/driverbox/config"
8 | lua "github.com/yuin/gopher-lua"
9 | "go.uber.org/zap"
10 | )
11 |
12 | // ToJSON 设备数据转 json
13 | func (d DeviceData) ToJSON() string {
14 | b, _ := json.Marshal(d)
15 | return string(b)
16 | }
17 |
18 | // Plugin 驱动插件
19 | type Plugin interface {
20 | // Initialize 初始化日志、配置、接收回调
21 | Initialize(logger *zap.Logger, c config.Config, ls *lua.LState)
22 | // Connector 连接器
23 | Connector(deviceId string) (connector Connector, err error)
24 | // Destroy 销毁驱动
25 | Destroy() error
26 | }
27 |
28 | // Connector 连接器
29 | type Connector interface {
30 | Encode(deviceId string, mode EncodeMode, values ...PointData) (res interface{}, err error) // 编码,是否支持批量的读写操作,由各插件觉得
31 | Send(data interface{}) (err error) // 发送数据
32 | Release() (err error) // 释放连接资源
33 | }
34 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/bvlc.go:
--------------------------------------------------------------------------------
1 | package btypes
2 |
3 | // BACnet Virtual Link Control (BVLC)
4 |
5 | // BVLCTypeBacnetIP is the only valid type for the BVLC layer as of 2002.
6 | // Additional btypes may be added in the future
7 | const BVLCTypeBacnetIP = 0x81
8 |
9 | // BacFunc Function
10 | type BacFunc byte
11 |
12 | // List of possible BACnet functions
13 | const (
14 | BacFuncResult BacFunc = 0
15 | BacFuncWriteBroadcastDistributionTable BacFunc = 1
16 | BacFuncBroadcastDistributionTable BacFunc = 2
17 | BacFuncBroadcastDistributionTableAck BacFunc = 3
18 | BacFuncForwardedNPDU BacFunc = 4
19 | BacFuncUnicast BacFunc = 10
20 | BacFuncBroadcast BacFunc = 11
21 | )
22 |
23 | type BVLC struct {
24 | Type byte
25 | Function BacFunc
26 |
27 | // Length includes the length of Type, Function, and Length. (4 bytes) It also
28 | // has the length of the data field after
29 | Length uint16
30 | Data []byte
31 | }
32 |
--------------------------------------------------------------------------------
/README.en.md:
--------------------------------------------------------------------------------
1 | # DriverBox
2 |
3 | ## Document
4 |
5 | [Quick start](https://ibuilding-x.github.io/driver-box/)
6 |
7 |
8 | ## Install
9 |
10 | 1. Download The Source Code
11 |
12 | ```bash
13 | git clone https://gitee.com/iBUILDING-X/driver-box.git
14 | ```
15 |
16 | 2. Load GO dependencies
17 |
18 | ```bash
19 | cd driver-box
20 | go mod vendor # 国内用户可以切换源:go env -w GOPROXY=https://goproxy.cn,direct
21 | ```
22 |
23 | ## Run locally
24 |
25 | 1. Open the main.go file
26 |
27 | ```go
28 | func main() {
29 | driverbox.Start([]export.Export{&export.DefaultExport{}})
30 | select {}
31 | }
32 | ```
33 |
34 | 2. Start the driver box
35 |
36 | ```bash
37 | go run main.go
38 | ```
39 |
40 | ## Participate and contribute
41 |
42 | 1. Fork's own warehouse
43 | 2. Create a new Feat_xxx branch
44 | 3. Submit code
45 | 4. Create a new Pull Request
46 |
47 | ## Feedback
48 |
49 | If you have any questions, please contact [issues](https://gitee.com/iBUILDING-X/driver-box/issues) Quick feedback
50 |
51 | ## Thank
52 |
53 | - [EdgeX Foundry](https://www.edgexfoundry.org/)
54 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/bacerr/errorclass_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type=ErrorClass"; DO NOT EDIT.
2 |
3 | package bacerr
4 |
5 | import "strconv"
6 |
7 | func _() {
8 | // An "invalid array index" compiler error signifies that the constant values have changed.
9 | // Re-run the stringer command to generate them again.
10 | var x [1]struct{}
11 | _ = x[DeviceError-0]
12 | _ = x[ObjectError-1]
13 | _ = x[PropertyError-2]
14 | _ = x[ResourcesError-3]
15 | _ = x[SecurityError-4]
16 | _ = x[ServicesError-5]
17 | _ = x[VTError-6]
18 | _ = x[CommunicationError-7]
19 | }
20 |
21 | const _ErrorClass_name = "DeviceErrorObjectErrorPropertyErrorResourcesErrorSecurityErrorServicesErrorVTErrorCommunicationError"
22 |
23 | var _ErrorClass_index = [...]uint8{0, 11, 22, 35, 49, 62, 75, 82, 100}
24 |
25 | func (i ErrorClass) String() string {
26 | if i >= ErrorClass(len(_ErrorClass_index)-1) {
27 | return "ErrorClass(" + strconv.FormatInt(int64(i), 10) + ")"
28 | }
29 | return _ErrorClass_name[_ErrorClass_index[i]:_ErrorClass_index[i+1]]
30 | }
31 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.18-alpine AS builder
2 |
3 | ENV GO111MODULE=on
4 | ENV GOPROXY=https://goproxy.cn,direct
5 |
6 | WORKDIR /build
7 |
8 | COPY ./driver-config ./driver-config
9 | COPY ./driverbox ./driverbox
10 | COPY ./internal ./internal
11 | COPY ./go.sum ./go.sum
12 | COPY ./go.mod ./go.mod
13 | COPY ./main.go ./main.go
14 |
15 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
16 | # apk update && apk add pkgconfig zeromq-dev gcc libc-dev && \
17 | go mod tidy && \
18 | go mod vendor && \
19 | go build -o driver-box .
20 |
21 | FROM alpine:latest
22 |
23 | WORKDIR /
24 |
25 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \
26 | apk update && apk add curl tzdata && \
27 | cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
28 | echo "Asia/Shanghai" > /etc/timezone && \
29 | apk del tzdata && \
30 | rm -rf /var/cache/apk/*
31 |
32 | COPY --from=builder /build/driver-box /driver-box
33 |
34 | EXPOSE 59999
35 |
36 | ENTRYPOINT ["/driver-box"]
--------------------------------------------------------------------------------
/internal/plugins/dlt645/core/dltcon/proc.go:
--------------------------------------------------------------------------------
1 | package dltcon
2 |
3 | // Handler 处理函数
4 | type Handler interface {
5 | ProcReadCoils(slaveID byte, address, quality uint16, valBuf []byte)
6 | ProcReadDiscretes(slaveID byte, address, quality uint16, valBuf []byte)
7 | ProcReadHoldingRegisters(slaveID byte, address, quality uint16, valBuf []byte)
8 | ProcReadInputRegisters(slaveID byte, address, quality uint16, valBuf []byte)
9 | ProcResult(err error, result *Result)
10 | }
11 |
12 | type NopProc struct{}
13 |
14 | func (NopProc) ProcReadCoils(byte, uint16, uint16, []byte) {}
15 | func (NopProc) ProcReadDiscretes(byte, uint16, uint16, []byte) {}
16 | func (NopProc) ProcReadHoldingRegisters(byte, uint16, uint16, []byte) {}
17 | func (NopProc) ProcReadInputRegisters(byte, uint16, uint16, []byte) {}
18 | func (NopProc) ProcResult(_ error, result *Result) {
19 | //log.Printf("Tx=%d,Err=%d,SlaveID=%d,FC=%d,Address=%d,Quantity=%d,SR=%dms",
20 | // result.TxCnt, result.ErrCnt, result.SlaveID, result.FuncCode,
21 | // result.Address, result.Quantity, result.ScanRate/time.Millisecond)
22 | }
23 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/mock.go:
--------------------------------------------------------------------------------
1 | package dlt645
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/ibuilding-x/driver-box/driverbox/helper"
6 | lua "github.com/yuin/gopher-lua"
7 | "go.uber.org/zap"
8 | )
9 |
10 | func (c *connector) mockRead(slaveId uint8, registerType string, address, quantity uint16) (values []uint16, err error) {
11 | mockData, err := helper.CallLuaMethod(c.plugin.ls, "mockRead", lua.LNumber(slaveId), lua.LString(registerType), lua.LNumber(address), lua.LNumber(quantity))
12 | if err != nil {
13 | return
14 | }
15 | err = json.Unmarshal([]byte(mockData), &values)
16 | return
17 | }
18 |
19 | func (c *connector) mockWrite(slaveID uint8, registerType primaryTable, address uint16, values []uint16) error {
20 | valueTable := c.plugin.ls.NewTable()
21 | for _, v := range values {
22 | valueTable.Append(lua.LNumber(v))
23 | }
24 | result, err := helper.CallLuaMethod(c.plugin.ls, "mockWrite", lua.LNumber(slaveID), lua.LString(registerType), lua.LNumber(address), valueTable)
25 | if err == nil {
26 | helper.Logger.Info("mockWrite result", zap.Any("result", result))
27 | }
28 | return err
29 | }
30 |
--------------------------------------------------------------------------------
/internal/plugins/modbus/mock.go:
--------------------------------------------------------------------------------
1 | package modbus
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/ibuilding-x/driver-box/driverbox/helper"
6 | lua "github.com/yuin/gopher-lua"
7 | "go.uber.org/zap"
8 | )
9 |
10 | func (c *connector) mockRead(slaveId uint8, registerType string, address, quantity uint16) (values []uint16, err error) {
11 | mockData, err := helper.CallLuaMethod(c.plugin.ls, "mockRead", lua.LNumber(slaveId), lua.LString(registerType), lua.LNumber(address), lua.LNumber(quantity))
12 | if err != nil {
13 | return
14 | }
15 | err = json.Unmarshal([]byte(mockData), &values)
16 | return
17 | }
18 |
19 | func (c *connector) mockWrite(slaveID uint8, registerType primaryTable, address uint16, values []uint16) error {
20 | valueTable := c.plugin.ls.NewTable()
21 | for _, v := range values {
22 | valueTable.Append(lua.LNumber(v))
23 | }
24 | result, err := helper.CallLuaMethod(c.plugin.ls, "mockWrite", lua.LNumber(slaveID), lua.LString(registerType), lua.LNumber(address), valueTable)
25 | if err == nil {
26 | helper.Logger.Info("mockWrite result", zap.Any("result", result))
27 | }
28 | return err
29 | }
30 |
--------------------------------------------------------------------------------
/driverbox/helper/script.go:
--------------------------------------------------------------------------------
1 | package helper
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/common"
5 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
6 | "github.com/ibuilding-x/driver-box/internal/lua"
7 | glua "github.com/yuin/gopher-lua"
8 | "path/filepath"
9 | )
10 |
11 | // CallLuaConverter 调用 Lua 脚本转换器
12 | func CallLuaConverter(L *glua.LState, method string, raw interface{}) ([]plugin.DeviceData, error) {
13 | return lua.CallLuaConverter(L, method, raw)
14 | }
15 |
16 | // 执行指定lua方法
17 | func CallLuaMethod(L *glua.LState, method string, args ...glua.LValue) (string, error) {
18 | return lua.CallLuaMethod(L, method, args...)
19 | }
20 |
21 | // CallLuaEncodeConverter 调用 Lua 脚本编码转换器
22 | func CallLuaEncodeConverter(L *glua.LState, deviceSn string, raw interface{}) (string, error) {
23 | return lua.CallLuaEncodeConverter(L, deviceSn, raw)
24 | }
25 |
26 | // 关闭Lua虚拟机
27 | func Close(L *glua.LState) {
28 | lua.Close(L)
29 | }
30 |
31 | // scriptExists 判断lua脚本是否存在
32 | func ScriptExists(dir string) bool {
33 | return common.FileExists(filepath.Join(EnvConfig.ConfigPath, dir, common.LuaScriptName))
34 | }
35 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/datetime.go:
--------------------------------------------------------------------------------
1 | package btypes
2 |
3 | type DayOfWeek int
4 |
5 | const (
6 | None DayOfWeek = iota
7 | Monday DayOfWeek = iota
8 | Tuesday DayOfWeek = iota
9 | Wednesday DayOfWeek = iota
10 | Thursday DayOfWeek = iota
11 | Friday DayOfWeek = iota
12 | Saturday DayOfWeek = iota
13 | Sunday DayOfWeek = iota
14 | )
15 |
16 | type Date struct {
17 | Year int
18 | Month int
19 | Day int
20 | // Bacnet has an option to only do operations on even or odd months
21 | EvenMonth bool
22 | OddMonth bool
23 | EvenDay bool
24 | OddDay bool
25 | LastDayOfMonth bool
26 | DayOfWeek DayOfWeek
27 | }
28 |
29 | type Time struct {
30 | Hour int
31 | Minute int
32 | Second int
33 | Millisecond int
34 | }
35 |
36 | type DataTime struct {
37 | Date
38 | Time
39 | }
40 |
41 | // UnspecifiedTime means that this time is triggered through out a period. An
42 | // example of this is 02:FF:FF:FF will trigger all through out 2 am
43 | const UnspecifiedTime = 0xFF
44 |
45 | const (
46 | TimeStampTime = 0
47 | TimeStampSequence = 1
48 | TimeStampDatetime = 2
49 | )
50 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/iam.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
5 | )
6 |
7 | func (e *Encoder) IAm(id btypes.IAm) error {
8 | apdu := btypes.APDU{
9 | DataType: btypes.UnconfirmedServiceRequest,
10 | UnconfirmedService: btypes.ServiceUnconfirmedIAm,
11 | }
12 | e.write(apdu.DataType)
13 | e.write(apdu.UnconfirmedService)
14 |
15 | e.AppData(id.ID, false)
16 | e.AppData(id.MaxApdu, false)
17 | e.AppData(id.Segmentation, false)
18 | e.AppData(id.Vendor, false)
19 | return e.Error()
20 | }
21 |
22 | func (d *Decoder) IAm(id *btypes.IAm) error {
23 | objID, err := d.AppData()
24 | if err != nil {
25 | return err
26 | }
27 | if i, ok := objID.(btypes.ObjectID); ok {
28 | id.ID = i
29 | }
30 | maxapdu, _ := d.AppData()
31 | if m, ok := maxapdu.(uint32); ok {
32 | id.MaxApdu = m
33 | }
34 | segmentation, _ := d.AppData()
35 | if m, ok := segmentation.(uint32); ok {
36 | id.Segmentation = btypes.Enumerated(m)
37 | }
38 | vendor, err := d.AppData()
39 | if v, ok := vendor.(uint32); ok {
40 | id.Vendor = v
41 | }
42 | return d.Error()
43 | }
44 |
--------------------------------------------------------------------------------
/driverbox/event/event.go:
--------------------------------------------------------------------------------
1 | package event
2 |
3 | // todo 后续定义事件code采用 EventCode 类型
4 | type EventCode string
5 |
6 | const (
7 | //设备在离线状态事件
8 | EventCodeDeviceStatus = "deviceStatus"
9 | //driver-box服务状态
10 | EventCodeServiceStatus = "serviceStatus"
11 | //添加设备
12 | EventCodeAddDevice = "addDevice"
13 | //即将删除设备,在该事件中依旧可以查询设备信息
14 | EventCodeWillDeleteDevice = "willDeleteDevice"
15 | //即将执行ExportTo
16 | EventCodeWillExportTo = "willExportTo"
17 | //设备自动发现事件
18 | EventDeviceDiscover = "deviceDiscover"
19 |
20 | EventCodeLinkEdgeTrigger = "linkEdgeTrigger"
21 |
22 | // EventCodeOnOff 设备开关事件(空调的开关机、灯的开关……)
23 | EventCodeOnOff = "onOff"
24 |
25 | // EventCodePluginCallback 插件回调事件
26 | EventCodePluginCallback = "pluginCallback"
27 | )
28 |
29 | // 场景相关事件
30 | const (
31 | // UnknownDevice 未知设备
32 | UnknownDevice = "unknownDevice"
33 | // UnknownLinkEdge 未知场景
34 | UnknownLinkEdge = "unknownLinkEdge"
35 | )
36 |
37 | const (
38 | //服务启动成功
39 | ServiceStatusHealthy = "healthy"
40 | //服务启动异常
41 | ServiceStatusError = "error"
42 | )
43 |
44 | // Data 设备事件模型
45 | type Data struct {
46 | Code string `json:"code"` //事件Code
47 | Value interface{} `json:"value"`
48 | }
49 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/object_map.go:
--------------------------------------------------------------------------------
1 | package btypes
2 |
3 | import "encoding/json"
4 |
5 | type ObjectMap map[ObjectType]map[ObjectInstance]Object
6 |
7 | // Len returns the total number of entries within the object map.
8 | func (o ObjectMap) Len() int {
9 | counter := 0
10 | for _, t := range o {
11 | for _ = range t {
12 | counter++
13 | }
14 |
15 | }
16 | return counter
17 | }
18 |
19 | func (om ObjectMap) MarshalJSON() ([]byte, error) {
20 | m := make(map[string]map[ObjectInstance]Object)
21 | for typ, sub := range om {
22 | key := typ.String()
23 | if m[key] == nil {
24 | m[key] = make(map[ObjectInstance]Object)
25 | }
26 | for inst, obj := range sub {
27 | m[key][inst] = obj
28 | }
29 | }
30 | return json.Marshal(m)
31 | }
32 |
33 | func (om ObjectMap) UnmarshalJSON(data []byte) error {
34 | m := make(map[string]map[ObjectInstance]Object, 0)
35 | err := json.Unmarshal(data, &m)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | for t, sub := range m {
41 | key := GetType(t)
42 | if om[key] == nil {
43 | om[key] = make(map[ObjectInstance]Object)
44 | }
45 | for inst, obj := range sub {
46 | om[key][inst] = obj
47 | }
48 | }
49 | return nil
50 | }
51 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/helpers/ipbytes/ip.go:
--------------------------------------------------------------------------------
1 | package ip2bytes
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "errors"
7 | log "github.com/sirupsen/logrus"
8 | "math/big"
9 | "net"
10 | )
11 |
12 | func ip4toInt(ip4Address net.IP) int64 {
13 | IPv4Int := big.NewInt(0)
14 | IPv4Int.SetBytes(ip4Address.To4())
15 | return IPv4Int.Int64()
16 | }
17 |
18 | func pack32BinaryIP4(ip4Address string) []byte {
19 | ipv4Decimal := ip4toInt(net.ParseIP(ip4Address))
20 | buf := new(bytes.Buffer)
21 | err := binary.Write(buf, binary.BigEndian, uint32(ipv4Decimal))
22 | if err != nil {
23 | log.Errorln("helpers.mac.Pack32BinaryIP4() unable to write to buffer:", err)
24 | }
25 | return buf.Bytes()
26 | }
27 |
28 | /*
29 | New ip in byte format
30 | - ip with no subnet
31 | - port
32 | - returns uint8[192 168 15 10 186 192]
33 | */
34 | func New(ip string, port uint16) ([]uint8, error) {
35 | buf := new(bytes.Buffer)
36 | err := binary.Write(buf, binary.BigEndian, port)
37 | if err != nil {
38 | log.Errorln("helpers.mac.BuildMac() unable to write to binary:", err)
39 | return nil, errors.New("helpers.mac.BuildMac() unable to write to binary")
40 | }
41 | return append(pack32BinaryIP4(ip), buf.Bytes()...), nil
42 | }
43 |
--------------------------------------------------------------------------------
/driverbox/export/linkedge/action.go:
--------------------------------------------------------------------------------
1 | package linkedge
2 |
3 | // ActionType 执行动作类型
4 | type ActionType string
5 |
6 | const (
7 | // ActionTypeDevicePoint 执行类型:设置设备点位
8 | ActionTypeDevicePoint ActionType = "devicePoint"
9 | // ActionTypeLinkEdge 执行类型:触发场景联动
10 | ActionTypeLinkEdge ActionType = "linkEdge"
11 | )
12 |
13 | type Action struct {
14 | Type ActionType `json:"type"`
15 | // ACondition 执行条件
16 | Condition []Condition `json:"condition"`
17 | // Sleep 执行后休眠时长
18 | Sleep string `json:"sleep"`
19 | DevicePointAction
20 | SceneAction
21 | }
22 |
23 | // DevicePointAction 设备点位动作
24 | type DevicePointAction struct {
25 | // DeviceID 设备 ID
26 | DeviceID string `json:"devSn"`
27 | // DevicePoint 点位名称(兼容旧版本,后续版本将废弃)
28 | // Deprecated: 请使用 Points
29 | DevicePoint string `json:"point"`
30 | // Value 值(兼容旧版本,后续版本将废弃)
31 | // Deprecated: 请使用 Points
32 | Value interface{} `json:"value"`
33 | // Points 支持批量设置多个点位值
34 | Points []DevicePointActionItem `json:"points"`
35 | }
36 |
37 | // DevicePointActionItem 设备点位动作项
38 | type DevicePointActionItem struct {
39 | Point string `json:"point"`
40 | Value string `json:"value"`
41 | }
42 |
43 | // SceneAction 触发场景联动动作
44 | type SceneAction struct {
45 | ID string `json:"id"`
46 | }
47 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/ndpu/networkmessagetype_string.go:
--------------------------------------------------------------------------------
1 | // Code generated by "stringer -type=NetworkMessageType"; DO NOT EDIT.
2 |
3 | package ndpu
4 |
5 | import "strconv"
6 |
7 | func _() {
8 | // An "invalid array index" compiler error signifies that the constant values have changed.
9 | // Re-run the stringer command to generate them again.
10 | var x [1]struct{}
11 | _ = x[WhoIsRouterToNetwork-0]
12 | _ = x[IamRouterToNetwork-1]
13 | _ = x[WhatIsNetworkNumber-18]
14 | _ = x[NetworkIs-19]
15 | }
16 |
17 | const (
18 | _NetworkMessageType_name_0 = "WhoIsRouterToNetworkIamRouterToNetwork"
19 | _NetworkMessageType_name_1 = "WhatIsNetworkNumberNetworkIs"
20 | )
21 |
22 | var (
23 | _NetworkMessageType_index_0 = [...]uint8{0, 20, 38}
24 | _NetworkMessageType_index_1 = [...]uint8{0, 19, 28}
25 | )
26 |
27 | func (i NetworkMessageType) String() string {
28 | switch {
29 | case i <= 1:
30 | return _NetworkMessageType_name_0[_NetworkMessageType_index_0[i]:_NetworkMessageType_index_0[i+1]]
31 | case 18 <= i && i <= 19:
32 | i -= 18
33 | return _NetworkMessageType_name_1[_NetworkMessageType_index_1[i]:_NetworkMessageType_index_1[i+1]]
34 | default:
35 | return "NetworkMessageType(" + strconv.FormatInt(int64(i), 10) + ")"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/driverbox/pkg/mbserver/modbus/storage_test.go:
--------------------------------------------------------------------------------
1 | package modbus
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestStorage(t *testing.T) {
8 | cs := &coilStorage{}
9 | t.Run("coilStorage", func(t *testing.T) {
10 | err := cs.Write(100, []bool{true, false})
11 | if err != nil {
12 | t.Fatalf("coilStorage write failed: %v", err)
13 | }
14 |
15 | values, err := cs.Read(100, 2)
16 | if err != nil {
17 | t.Fatalf("coilStorage read failed: %v", err)
18 | }
19 | if len(values) != 2 {
20 | t.Fatalf("coilStorage read wrong length: got %d, want 2", len(values))
21 | }
22 | if values[0] != true || values[1] != false {
23 | t.Fatalf("coilStorage read failed")
24 | }
25 | })
26 |
27 | rs := ®isterStorage{}
28 | t.Run("registerStorage", func(t *testing.T) {
29 | err := rs.Write(100, []uint16{1, 2})
30 | if err != nil {
31 | t.Fatalf("registerStorage write failed: %v", err)
32 | }
33 |
34 | values, err := rs.Read(100, 2)
35 | if err != nil {
36 | t.Fatalf("registerStorage read failed: %v", err)
37 | }
38 | if len(values) != 2 {
39 | t.Fatalf("registerStorage read wrong length: got %d, want 2", len(values))
40 | }
41 | if values[0] != 1 || values[1] != 2 {
42 | t.Fatalf("registerStorage read failed")
43 | }
44 | })
45 | }
46 |
--------------------------------------------------------------------------------
/driverbox/export.go:
--------------------------------------------------------------------------------
1 | package driverbox
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/export"
5 | export0 "github.com/ibuilding-x/driver-box/internal/export"
6 | )
7 |
8 | var Exports exports
9 |
10 | // exports 结构体用于管理driver-box框架中的所有Export插件
11 | // 提供加载单个、批量加载以及加载所有内置Export的方法
12 | type exports struct {
13 | }
14 |
15 | // LoadExport 加载单个自定义Export插件
16 | // 参数:
17 | //
18 | // export2: 需要加载的Export插件实例
19 | //
20 | // 功能:
21 | //
22 | // 如果该Export尚未加载,则将其添加到全局Exports列表中
23 | func (exports *exports) LoadExport(export2 export.Export) {
24 | if !exports.exists(export2) {
25 | export0.Exports = append(export0.Exports, export2)
26 | }
27 | }
28 |
29 | // LoadExports 批量加载多个Export插件
30 | // 参数:
31 | //
32 | // export2: 需要加载的Export插件实例数组
33 | //
34 | // 功能:
35 | //
36 | // 遍历数组并调用LoadExport方法逐个加载
37 | func (exports *exports) LoadExports(export2 []export.Export) {
38 | for _, e := range export2 {
39 | exports.LoadExport(e)
40 | }
41 | }
42 |
43 | // exists 检查指定的Export是否已经加载
44 | // 参数:
45 | //
46 | // exp: 需要检查的Export实例
47 | //
48 | // 返回值:
49 | //
50 | // bool: true表示已加载,false表示未加载
51 | func (exports *exports) exists(exp export.Export) bool {
52 | for _, e := range export0.Exports {
53 | if e == exp {
54 | return true
55 | }
56 | }
57 | return false
58 | }
59 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/helpers/validation/vaildation.go:
--------------------------------------------------------------------------------
1 | package validation
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "regexp"
7 | "strings"
8 | )
9 |
10 | func ValidCIDR(ip string, cidr int) (ok bool) {
11 | ip = fmt.Sprintf("%s/%d", ip, cidr)
12 | _, _, err := net.ParseCIDR(ip)
13 | if err != nil {
14 | return
15 | }
16 | ok = true
17 | return
18 | }
19 |
20 | func ValidPort(port int) bool {
21 | t := fmt.Sprintf("%d", port)
22 | re, _ := regexp.Compile(`^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$`)
23 | if re.MatchString(t) {
24 | return true
25 | }
26 | return false
27 | }
28 |
29 | // ValidIP return true if string ip contains a valid representation of an IPv4 or IPv6 address
30 | func ValidIP(ip string) bool {
31 | ipaddr := net.ParseIP(NormaliseIPAddr(ip))
32 | return ipaddr != nil
33 | }
34 |
35 | // NormaliseIPAddr return ip address without /32 (IPv4 or /128 (IPv6)
36 | func NormaliseIPAddr(ip string) string {
37 | if strings.HasSuffix(ip, "/32") && strings.Contains(ip, ".") { // single host (IPv4)
38 | ip = strings.TrimSuffix(ip, "/32")
39 | } else {
40 | if strings.HasSuffix(ip, "/128") { // single host (IPv6)
41 | ip = strings.TrimSuffix(ip, "/128")
42 | }
43 | }
44 |
45 | return ip
46 | }
47 |
--------------------------------------------------------------------------------
/test/library_test.go:
--------------------------------------------------------------------------------
1 | package test
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/library"
5 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
6 | "github.com/ibuilding-x/driver-box/internal/logger"
7 | "go.uber.org/zap"
8 | "testing"
9 | )
10 |
11 | func TestDeviceEncode(t *testing.T) {
12 | Init()
13 | result := library.Driver().DeviceEncode("test_2", library.DeviceEncodeRequest{
14 | DeviceId: "switch_1",
15 | Mode: plugin.WriteMode,
16 | Points: []plugin.PointData{
17 | {
18 | PointName: "aa",
19 | Value: int64(6),
20 | },
21 | },
22 | })
23 | if result.Error != nil {
24 | t.Error(result.Error)
25 | return
26 | }
27 | logger.Logger.Info("result", zap.Any("result", result))
28 | }
29 |
30 | func TestDeviceDecode(t *testing.T) {
31 | Init()
32 | result := library.Driver().DeviceDecode("test_1", library.DeviceDecodeRequest{
33 | DeviceId: "test_1",
34 | Points: []plugin.PointData{
35 | {
36 | PointName: "aa",
37 | Value: 123123,
38 | },
39 | {
40 | PointName: "bb",
41 | Value: 1000,
42 | },
43 | {
44 | PointName: "cc",
45 | Value: 324,
46 | },
47 | },
48 | })
49 | if result.Error != nil {
50 | t.Error(result.Error)
51 | return
52 | }
53 | logger.Logger.Info("result", zap.Any("result", result))
54 | }
55 |
--------------------------------------------------------------------------------
/internal/export/ai/mcp/tools/shadow.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "github.com/ibuilding-x/driver-box/driverbox/helper"
7 | "github.com/mark3labs/mcp-go/mcp"
8 | )
9 |
10 | var ShadowDeviceListTool = mcp.NewTool("device_shadow_list",
11 | mcp.WithDescription("获取网关中的所有设备影子数据,返回JSON格式的设备影子列表。设备影子是设备状态的虚拟表示,包含设备的最新状态信息、点位值和连接状态等数据,可用于了解设备的实时状态。"),
12 | )
13 |
14 | var ShadowDeviceListHandler = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
15 | devices := helper.DeviceShadow.GetDevices()
16 | jsonData, _ := json.Marshal(devices)
17 | return mcp.NewToolResultText(string(jsonData)), nil
18 | }
19 |
20 | var ShadowDeviceTool = mcp.NewTool("device_shadow_info",
21 | mcp.WithDescription("获取网关中指定设备ID的影子数据,返回JSON格式的单个设备影子信息。通过设备ID可以查询特定设备的实时状态、点位值和连接状态等详细信息,便于针对性地分析和监控设备。"),
22 | mcp.WithString("id", mcp.Required(), mcp.Description("设备唯一标识符,用于查询特定设备的影子数据")),
23 | )
24 |
25 | var ShadowDeviceHandler = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
26 | id, err := request.RequireString("id")
27 | if err != nil {
28 | return mcp.NewToolResultError(err.Error()), nil
29 | }
30 | devices, _ := helper.DeviceShadow.GetDevice(id)
31 | jsonData, _ := json.Marshal(devices)
32 | return mcp.NewToolResultText(string(jsonData)), nil
33 | }
34 |
--------------------------------------------------------------------------------
/internal/plugins/manage.go:
--------------------------------------------------------------------------------
1 | // 插件管理器
2 |
3 | package plugins
4 |
5 | import (
6 | "fmt"
7 | "sync"
8 |
9 | "github.com/ibuilding-x/driver-box/driverbox/config"
10 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
11 | )
12 |
13 | // Manager 插件管理器
14 | var Manager *manager
15 |
16 | func init() {
17 | Manager = &manager{
18 | plugins: &sync.Map{},
19 | }
20 | }
21 |
22 | // manager 管理器
23 | type manager struct {
24 | plugins *sync.Map
25 | }
26 |
27 | // 注册自定义插件
28 | func (m *manager) Register(name string, plugin plugin.Plugin) {
29 | if _, ok := m.plugins.Load(name); ok {
30 | fmt.Printf("plugin %s already exists, replace it", name)
31 | }
32 | fmt.Printf("register plugin: %s\n", name)
33 | m.plugins.Store(name, plugin)
34 | }
35 |
36 | // Get 获取插件实例
37 | func (m *manager) Get(c config.Config) (p plugin.Plugin, err error) {
38 | if raw, ok := m.plugins.Load(c.ProtocolName); ok {
39 | p = raw.(plugin.Plugin)
40 | } else {
41 | err = fmt.Errorf("plugin:[%s] not found", c.ProtocolName)
42 | }
43 | return
44 | }
45 |
46 | func (m *manager) GetSupportPlugins() []string {
47 | plugins := make([]string, 0)
48 | m.plugins.Range(func(key, value interface{}) bool {
49 | plugins = append(plugins, key.(string))
50 | return true
51 | })
52 | return plugins
53 | }
54 |
55 | func (m *manager) Clear() {
56 | m.plugins = &sync.Map{}
57 | }
58 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/network/write.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
6 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes/null"
7 | )
8 |
9 | type Write struct {
10 | DeviceId string
11 | PointName string
12 | ObjectID btypes.ObjectInstance
13 | ObjectType btypes.ObjectType
14 | Prop btypes.PropertyType
15 | WriteValue interface{}
16 | WriteNull bool
17 | WritePriority uint8
18 | }
19 |
20 | func (device *Device) Write(write *Write) error {
21 | var err error
22 | writeValue := write.WriteValue
23 |
24 | rp := btypes.PropertyData{
25 | Object: btypes.Object{
26 | ID: btypes.ObjectID{
27 | Type: write.ObjectType,
28 | Instance: write.ObjectID,
29 | },
30 | Properties: []btypes.Property{
31 | {
32 | Type: write.Prop,
33 | ArrayIndex: bacnet.ArrayAll,
34 | Priority: btypes.NPDUPriority(write.WritePriority),
35 | },
36 | },
37 | },
38 | }
39 |
40 | if write.WriteNull {
41 | writeValue = null.Null{}
42 | }
43 |
44 | rp.Object.Properties[0].Data = writeValue
45 |
46 | err = device.network.WriteProperty(device.dev, rp)
47 |
48 | if err != nil {
49 | return err
50 | }
51 | return nil
52 | }
53 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/network/discover_test.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "fmt"
5 | pprint "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/helpers/print"
6 |
7 | //"github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet"
8 |
9 | "testing"
10 | )
11 |
12 | func TestDiscover(t *testing.T) {
13 |
14 | localDevice, err := New(&Network{Interface: iface, Port: 47808})
15 | if err != nil {
16 | fmt.Println("ERR-client", err)
17 | return
18 | }
19 | defer localDevice.NetworkClose()
20 | go localDevice.NetworkRun()
21 |
22 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID})
23 | if err != nil {
24 | return
25 | }
26 |
27 | objects, err := device.DeviceObjects(202, true)
28 | if err != nil {
29 | return
30 | }
31 | pprint.PrintJOSN(objects)
32 |
33 | }
34 |
35 | func TestGetPointsList(t *testing.T) {
36 |
37 | localDevice, err := New(&Network{Interface: iface, Port: 47808})
38 | if err != nil {
39 | fmt.Println("ERR-client", err)
40 | return
41 | }
42 | defer localDevice.NetworkClose()
43 | go localDevice.NetworkRun()
44 |
45 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID})
46 | if err != nil {
47 | return
48 | }
49 |
50 | objects, err := device.GetDevicePoints(202)
51 | if err != nil {
52 | return
53 | }
54 | pprint.PrintJOSN(objects)
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/driverbox/library/device_model.go:
--------------------------------------------------------------------------------
1 | package library
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/ibuilding-x/driver-box/driverbox/common"
7 | "github.com/ibuilding-x/driver-box/driverbox/config"
8 | "io/fs"
9 | "path"
10 | "path/filepath"
11 | "strings"
12 | )
13 |
14 | type DeviceModel struct {
15 | }
16 |
17 | // 加载指定key的驱动
18 | func (device *DeviceModel) LoadLibrary(modelKey string) (config.DeviceModel, error) {
19 | filePath := path.Join(config.ResourcePath, baseDir, string(deviceModel), modelKey+".json")
20 | if !common.FileExists(filePath) {
21 | return config.DeviceModel{}, fmt.Errorf("device model library not found: %s", modelKey)
22 | }
23 | //读取filePath中的文件内容
24 | bytes, e := common.ReadFileBytes(filePath)
25 | if e != nil {
26 | return config.DeviceModel{}, e
27 | }
28 | model := config.DeviceModel{}
29 | e = json.Unmarshal(bytes, &model)
30 | return model, e
31 | }
32 |
33 | // 列出所有物模型
34 | func (device *DeviceModel) ListModels() []string {
35 | modelPath := path.Join(config.ResourcePath, baseDir, string(deviceModel))
36 | //获取 modelPath目录下的所有json文件名
37 | var files []string
38 | _ = filepath.WalkDir(modelPath, func(path string, d fs.DirEntry, err error) error {
39 | if err != nil {
40 | return err
41 | }
42 | if !d.IsDir() && strings.HasSuffix(d.Name(), ".json") {
43 | files = append(files, strings.TrimRight(d.Name(), ".json"))
44 | }
45 | return nil
46 | })
47 | return files
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/network/base.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet"
5 | log "github.com/sirupsen/logrus"
6 | )
7 |
8 | type Network struct {
9 | Interface string
10 | Ip string
11 | Port int
12 | SubnetCIDR int
13 | StoreID string
14 | Client bacnet.Client
15 | }
16 |
17 | // New returns a new instance of bacnet network
18 | func New(net *Network) (*Network, error) {
19 | cb := &bacnet.ClientBuilder{
20 | Interface: net.Interface,
21 | Ip: net.Ip,
22 | Port: net.Port,
23 | SubnetCIDR: net.SubnetCIDR,
24 | }
25 |
26 | bc, err := bacnet.NewClient(cb)
27 | if err != nil {
28 | return nil, err
29 | }
30 | net.Client = bc
31 | if BacStore != nil {
32 | BacStore.Set(net.StoreID, net, -1)
33 | }
34 | return net, nil
35 | }
36 |
37 | func (net *Network) NetworkClose() {
38 | if net.Client != nil {
39 | log.Infof("close bacnet network")
40 | err := net.Client.Close()
41 | if err != nil {
42 | log.Errorf("close bacnet network err:%s", err.Error())
43 | return
44 | }
45 | }
46 | }
47 |
48 | func (net *Network) IsRunning() bool {
49 | if net.Client != nil {
50 | return net.Client.IsRunning()
51 | }
52 | return false
53 | }
54 |
55 | func (net *Network) NetworkRun() {
56 | if net.Client != nil {
57 | go net.Client.ClientRun()
58 | }
59 | }
60 |
61 | // func (net *Network) store() {
62 |
63 | // }
64 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/network/strings_test.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "fmt"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
6 | "testing"
7 | )
8 |
9 | func TestDevice_ReadPointName(t *testing.T) {
10 |
11 | localDevice, err := New(&Network{Interface: iface, Port: 47808})
12 | if err != nil {
13 | fmt.Println("ERR-client", err)
14 | return
15 | }
16 | defer localDevice.NetworkClose()
17 | go localDevice.NetworkRun()
18 |
19 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID})
20 | if err != nil {
21 | return
22 | }
23 |
24 | pnt := &Point{
25 | ObjectID: 1,
26 | ObjectType: btypes.AnalogOutput,
27 | }
28 | read, err := device.ReadPointName(pnt)
29 | fmt.Println(err)
30 | fmt.Println(read, err)
31 |
32 | }
33 |
34 | func TestDevice_WritePointName(t *testing.T) {
35 |
36 | localDevice, err := New(&Network{Interface: iface, Port: 47808})
37 | if err != nil {
38 | fmt.Println("ERR-client", err)
39 | return
40 | }
41 | defer localDevice.NetworkClose()
42 | go localDevice.NetworkRun()
43 |
44 | device, err := NewDevice(localDevice, &Device{Ip: deviceIP, DeviceID: deviceID})
45 | if err != nil {
46 | return
47 | }
48 |
49 | pnt := &Point{
50 | ObjectID: 1,
51 | ObjectType: btypes.AnalogOutput,
52 | }
53 |
54 | err = device.WritePointName(pnt, "new-name")
55 | fmt.Println(err)
56 | if err != nil {
57 | //return
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/core/res/DataMarkerConfig.toml:
--------------------------------------------------------------------------------
1 | 00000000='当前组合有功总电能'
2 | 00010000='当前正向有功总电能'
3 | 00020000='当前反向有功总电能'
4 | 00030000='当前组合无功1总电能'
5 | 00040000='当前组合无功2总电能'
6 | 00050000='当前第一象限无功总电能'
7 | 00060000='当前第二象限无功总电能'
8 | 00070000='当前第三象限无功总电能'
9 | 00080000='当前第四象限无功总电能'
10 | 00000001='上1结算日组合有功总电能'
11 | 00010001='上1结算日正向有功总电能'
12 | 00020001='上1结算日反向有功总电能'
13 | 00030001='上1结算日组合无功1总电能'
14 | 00040001='上1结算日组合无功2总电能'
15 | 00050001='上1结算日第一象限无功总电能'
16 | 00060001='上1结算日第二象限无功总电能'
17 | 00070001='上1结算日第三象限无功总电能'
18 | 00080001='上1结算日第四象限无功总电能'
19 |
20 | 02010100='A相电压'
21 | 02010200='B相电压'
22 | 02010300='C相电压'
23 | 0201FF00='电压数据块'
24 | 02020100='A相电流'
25 | 02020200='B相电流'
26 | 02020300='C相电流'
27 | 0202FF00='电流数据块'
28 | 02030000='瞬时总有功功率'
29 | 02030100='瞬时A相有功功率'
30 | 02030200='瞬时B相有功功率'
31 | 02030300='瞬时C相有功功率'
32 | 02040000='瞬时总无功功率'
33 | 02040100='瞬时A相无功功率'
34 | 02040200='瞬时B相无功功率'
35 | 02040300='瞬时C相无功功率'
36 | 02060000='总功率因数'
37 | 02060100='A相功率因数'
38 | 02060200='B相功率因数'
39 | 02060300='C相功率因数'
40 | 02800001='零线电流'
41 | 02800002='电网频率'
42 | 02800003='一分钟有功总平均功率'
43 | 02800004='当前有功需量'
44 | 08000005='当前无功需量'
45 | 02050000='瞬时总视在功率'
46 | 02050100='瞬时A相视在功率'
47 | 02050200='瞬时B相视在功率'
48 | 02050300='瞬时C相视在功率'
49 |
50 | 04000101='日期及星期(年月日星期)'
51 | 04000102='时间(时分秒)'
52 | 04000303='显示电能小数位数(位)'
53 | 04000404='额定电压(ASCII码)'
54 | 04800001='厂家软件版本号(ASCII码)'
55 | 04800002='厂家硬件版本号(ASCII码)'
56 | 04800003='厂家编号(ASCII码)'
57 |
--------------------------------------------------------------------------------
/driverbox/restful/route/api.go:
--------------------------------------------------------------------------------
1 | package route
2 |
3 | const V1Prefix string = "/api/v1/"
4 |
5 | // 设备写操作
6 | const DevicePointWrite = V1Prefix + "device/writePoint"
7 |
8 | // 批量写入某个设备的点位
9 | const DevicePointsWrite = V1Prefix + "device/writePoints"
10 |
11 | const DevicePointRead = V1Prefix + "device/readPoint"
12 |
13 | // 查询设备列表
14 | const DeviceList = V1Prefix + "device/list"
15 |
16 | // 获取设备信息
17 | const DeviceGet = V1Prefix + "device/get"
18 |
19 | // 添加设备
20 | const DeviceAdd = V1Prefix + "device/add"
21 |
22 | // 删除设备
23 | const DeviceDelete = V1Prefix + "device/delete"
24 |
25 | // 创建场景联动
26 | const LinkEdgeCreate = V1Prefix + "linkedge/create"
27 |
28 | // 试运行场景,不作持久化
29 | const LinkEdgeTryTrigger = V1Prefix + "linkedge/try"
30 |
31 | // 删除场景联动
32 | const LinkEdgeDelete = V1Prefix + "linkedge/delete"
33 |
34 | // 触发指定ID的场景联动
35 | const LinkEdgeTrigger = V1Prefix + "linkedge/trigger"
36 |
37 | // 获取指定ID的场景联动配置
38 | const LinkEdgeGet = V1Prefix + "linkedge/get"
39 |
40 | // 获取场景联动列表
41 | const LinkEdgeList = V1Prefix + "linkedge/list"
42 |
43 | // 更新场景联动
44 | const LinkEdgeUpdate = V1Prefix + "linkedge/update"
45 |
46 | // 更新场景联动状态
47 | const LinkEdgeStatus = V1Prefix + "linkedge/status"
48 |
49 | // deprecated 获取最后一个执行的场景联动
50 | const LinkEdgeGetLast = V1Prefix + "linkedge/getLast"
51 |
52 | // modbus驱动--设备发现
53 | const ModbusDeviceDiscovery = V1Prefix + "plugin/modbus/discovery"
54 |
55 | // bacnet驱动--设备发现
56 | const BacnetDeviceDiscovery = V1Prefix + "plugin/bacnet/discovery"
57 |
--------------------------------------------------------------------------------
/driverbox/export/export.go:
--------------------------------------------------------------------------------
1 | package export
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
5 | )
6 |
7 | // Export 定义了驱动数据导出的标准接口
8 | // 实现该接口的模块可以将设备数据导出到不同目标(如EdgeX总线、MQTT等)
9 | // 该接口提供了驱动数据导出的核心功能,包括:
10 | // 1. 初始化导出模块
11 | // 2. 设备数据导出
12 | // 3. 事件处理回调
13 | // 4. 状态检查
14 | // 所有导出模块都需要实现此接口才能被driver-box框架加载和使用
15 | type Export interface {
16 | // Init 初始化导出模块
17 | // 该方法在导出模块加载时被调用,用于执行必要的初始化操作
18 | // 如: 建立连接、加载配置、注册路由等
19 | // 返回值:
20 | // error - 初始化过程中发生的错误,成功返回nil
21 | Init() error
22 |
23 | // ExportTo 导出设备数据
24 | // 该方法在设备数据发生变化时被调用,将数据推送到配置的目标
25 | // 参数:
26 | // deviceData - 包含设备ID、点位名称和值的设备数据结构
27 | // deviceData.ID: 设备唯一标识
28 | // deviceData.Values: 点位数据集合
29 | // 功能:
30 | // 将设备数据导出到配置的目标(如EdgeX总线、MQTT等)
31 | // 实现时应注意处理异常情况并记录日志
32 | ExportTo(deviceData plugin.DeviceData)
33 |
34 | // OnEvent 事件回调接口
35 | // 当框架触发特定事件时调用此方法
36 | // 参数:
37 | // eventCode - 事件代码,标识事件类型
38 | // 常见事件类型: 设备发现、场景联动触发等
39 | // key - 事件关联的键值
40 | // 通常是设备ID或场景ID
41 | // eventValue - 事件关联的值
42 | // 事件相关的数据,类型根据事件不同而变化
43 | // 返回值:
44 | // error - 处理事件过程中发生的错误,成功返回nil
45 | // 功能:
46 | // 处理特定事件触发的业务逻辑
47 | // 实现时应根据eventCode进行不同处理
48 | OnEvent(eventCode string, key string, eventValue interface{}) error
49 |
50 | // IsReady 检查导出模块是否就绪
51 | // 该方法用于检查导出模块是否已完成初始化并准备好处理数据
52 | // 返回值:
53 | // bool - true表示模块已就绪,false表示未就绪
54 | // 注意:
55 | // 框架会在调用ExportTo和OnEvent前检查此状态
56 | IsReady() bool
57 |
58 | // Destroy 退出服务
59 | Destroy() error
60 | }
61 |
--------------------------------------------------------------------------------
/internal/export/ui/export.go:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/config"
5 | "github.com/ibuilding-x/driver-box/driverbox/helper"
6 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
7 | "github.com/ibuilding-x/driver-box/driverbox/restful"
8 | "net/http"
9 | "os"
10 | "sync"
11 | )
12 |
13 | var driverInstance *Export
14 | var once = &sync.Once{}
15 |
16 | type Export struct {
17 | ready bool
18 | }
19 |
20 | func (export *Export) Init() error {
21 | if os.Getenv(config.ENV_EXPORT_UI_ENABLED) == "false" {
22 | helper.Logger.Warn("driver-box ui is disabled")
23 | return nil
24 | }
25 | restful.HttpRouter.GET("/ui/", devices)
26 | restful.HttpRouter.GET("/ui/device/:deviceId", deviceDetail)
27 | //静态资源文件
28 | restful.HttpRouter.ServeFiles("/ui/css/*filepath", http.Dir("./res/ui/css"))
29 | restful.HttpRouter.ServeFiles("/ui/js/*filepath", http.Dir("./res/ui/js"))
30 | export.ready = true
31 | return nil
32 | }
33 | func NewExport() *Export {
34 | once.Do(func() {
35 | driverInstance = &Export{}
36 | })
37 |
38 | return driverInstance
39 | }
40 | func (export *Export) Destroy() error {
41 | export.ready = false
42 | return nil
43 | }
44 |
45 | // 点位变化触发场景联动
46 | func (export *Export) ExportTo(deviceData plugin.DeviceData) {
47 | }
48 |
49 | // 继承Export OnEvent接口
50 | func (export *Export) OnEvent(eventCode string, key string, eventValue interface{}) error {
51 |
52 | return nil
53 | }
54 |
55 | func (export *Export) IsReady() bool {
56 | return export.ready
57 | }
58 |
--------------------------------------------------------------------------------
/driverbox/pkg/mbserver/modbus/server.go:
--------------------------------------------------------------------------------
1 | package modbus
2 |
3 | import "io"
4 |
5 | type ServerHandler interface {
6 | Listen() error
7 | Close() error
8 | HandleFunc(func(message *ProtocolMessage))
9 | Send(message *ProtocolMessage) error
10 | }
11 |
12 | type Transporter interface {
13 | io.ReadWriter
14 | Connect() error
15 | Close() error
16 | }
17 |
18 | type Packager interface {
19 | Encode(message ProtocolMessage) (bs []byte, err error)
20 | Decode(bs []byte) (message ProtocolMessage, err error)
21 | }
22 |
23 | type ProtocolMessage struct {
24 | TransactionIdentifier uint16 `json:"transactionIdentifier"` // tcp: transaction identifier
25 | ProtocolIdentifier uint16 `json:"protocolIdentifier"` // tcp: protocol identifier
26 | Length uint16 `json:"length"` // tcp: length of the following data
27 | UnitIdentifier byte `json:"unitIdentifier"` // tcp: unit identifier
28 | SlaveId byte `json:"slaveId"` // slave address
29 | FunctionCode byte `json:"functionCode"` // function code
30 | Address uint16 `json:"address"` // start address
31 | Quantity uint16 `json:"quantity"` // quantity
32 | ValueLength byte `json:"valueLength"` // value length
33 | Values []byte `json:"values"` // value bytes
34 | ErrorCode byte `json:"errorCode"` // error code
35 | original []byte // original ADU
36 | }
37 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/string.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import (
4 | "fmt"
5 | "golang.org/x/text/encoding/unicode"
6 | )
7 |
8 | type stringType uint8
9 |
10 | // Supported String btypes
11 | const (
12 | //https://github.com/stargieg/bacnet-stack/blob/master/include/bacenum.h#L1261
13 | stringUTF8 stringType = 0 //same as ANSI_X34
14 | characterUCS2 stringType = 4 //johnson controllers use this
15 | )
16 |
17 | func (e *Encoder) string(s string) {
18 | e.write(stringUTF8)
19 | e.write([]byte(s))
20 | }
21 |
22 | func decodeUCS2(s string) (string, error) {
23 | dec := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder()
24 | out, err := dec.String(s)
25 | if err != nil {
26 | return "", err
27 | }
28 | return out, err
29 |
30 | }
31 |
32 | func (d *Decoder) string(s *string, len int) error {
33 | var t stringType
34 | d.decode(&t)
35 | switch t {
36 | case stringUTF8:
37 | case characterUCS2:
38 | default:
39 | return fmt.Errorf("unsupported string format %d", t)
40 | }
41 | b := make([]byte, len)
42 | d.decode(b)
43 |
44 | if t == characterUCS2 {
45 | out, err := decodeUCS2(string(b))
46 | if err != nil {
47 | return fmt.Errorf("unable to decode string format characterUCS2%d", t)
48 | }
49 | *s = out
50 | } else {
51 | *s = string(b)
52 | }
53 |
54 | return d.Error()
55 |
56 | }
57 | func (e *Encoder) octetstring(b []byte) {
58 | e.write([]byte(b))
59 | }
60 | func (d *Decoder) octetstring(b *[]byte, len int) {
61 | *b = make([]byte, len)
62 | d.decode(b)
63 | }
64 |
--------------------------------------------------------------------------------
/.github/workflows/pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to GitHub Pages
2 |
3 | on:
4 | # Trigger the workflow every time you push to the `main` branch
5 | # Using a different branch name? Replace `main` with your branch’s name
6 | push:
7 | # branches: [20240717_v1.1.0_dev]
8 | paths:
9 | - 'pages/**'
10 | # Allows you to run this workflow manually from the Actions tab on GitHub.
11 | workflow_dispatch:
12 |
13 | # Allow this job to clone the repo and create a page deployment
14 | permissions:
15 | contents: read
16 | pages: write
17 | id-token: write
18 |
19 | jobs:
20 | build:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - name: Checkout your repository using git
24 | uses: actions/checkout@v4
25 | - name: Install, build, and upload your site output
26 | uses: withastro/action@v2
27 | with:
28 | path: pages # The root location of your Astro project inside the repository. (optional)
29 | node-version: 22.0.0 # The specific version of Node that should be used to build your site. Defaults to 18. (optional)
30 | package-manager: yarn@ # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional)
31 |
32 | deploy:
33 | needs: build
34 | runs-on: ubuntu-latest
35 | environment:
36 | name: github-pages
37 | url: ${{ steps.deployment.outputs.page_url }}
38 | steps:
39 | - name: Deploy to GitHub Pages
40 | id: deployment
41 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/writeprop.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
5 | )
6 |
7 | // WriteProperty encodes a write request
8 | func (e *Encoder) WriteProperty(invokeID uint8, data btypes.PropertyData) error {
9 | a := btypes.APDU{
10 | DataType: btypes.ConfirmedServiceRequest,
11 | Service: btypes.ServiceConfirmedWriteProperty,
12 | MaxSegs: 0,
13 | MaxApdu: MaxAPDU,
14 | InvokeId: invokeID,
15 | }
16 | e.APDU(a)
17 |
18 | tagID, err := e.readPropertyHeader(0, &data)
19 | if err != nil {
20 | return err
21 | }
22 |
23 | prop := data.Object.Properties[0]
24 |
25 | if data.Object.ID.Type == 1 {
26 |
27 | }
28 |
29 | // Tag 3 - the value (unlike other values, this is just a raw byte array)
30 | e.openingTag(tagID)
31 | e.AppData(prop.Data, pointTypeBOBV(data))
32 | e.closingTag(tagID)
33 | tagID++
34 | // Tag 4 - Optional priority tag
35 | // Priority set
36 | if prop.Priority != btypes.Normal {
37 | e.contextUnsigned(tagID, uint32(prop.Priority))
38 | }
39 | return e.Error()
40 | }
41 |
42 | // pointTypeBOBV if point type is bv or bo then we need to set the data type to enum
43 | func pointTypeBOBV(data btypes.PropertyData) (isBool bool) {
44 | pointType := data.Object.ID.Type
45 | property := 0
46 | if len(data.Object.Properties) > 0 {
47 | property = int(data.Object.Properties[0].Type)
48 | }
49 | if (pointType == btypes.TypeBinaryValue || pointType == btypes.TypeBinaryOutput) && property == 85 {
50 | return true
51 | }
52 | return
53 | }
54 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/whois.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
5 | )
6 |
7 | func (e *Encoder) WhoIs(low, high int32) error {
8 | apdu := btypes.APDU{
9 | DataType: btypes.UnconfirmedServiceRequest,
10 | UnconfirmedService: btypes.ServiceUnconfirmedWhoIs,
11 | }
12 | e.write(apdu.DataType)
13 | e.write(apdu.UnconfirmedService)
14 |
15 | // The range is optional. A scan for all objects is done when either low/high
16 | // are negative or when we are scanning above the max instance
17 | if low >= 0 && high >= 0 && low < btypes.MaxInstance && high <
18 | btypes.MaxInstance {
19 | // Tag 0
20 | e.contextUnsigned(0, uint32(low))
21 |
22 | // Tag 1
23 | e.contextUnsigned(1, uint32(high))
24 | }
25 | return e.Error()
26 | }
27 |
28 | func (d *Decoder) WhoIs(low, high *int32) error {
29 | // APDU read in a higher level
30 | if d.len() == 0 {
31 | *low = btypes.WhoIsAll
32 | *high = btypes.WhoIsAll
33 | return nil
34 | }
35 | // Tag 0 - Low Value
36 | var expectedTag uint8
37 | tag, _, value := d.tagNumberAndValue()
38 | if tag != expectedTag {
39 | return &ErrorIncorrectTag{Expected: expectedTag, Given: tag}
40 | }
41 | l := d.unsigned(int(value))
42 | *low = int32(l)
43 |
44 | // Tag 1 - High Value
45 | expectedTag = 1
46 | tag, _, value = d.tagNumberAndValue()
47 | if tag != expectedTag {
48 | return &ErrorIncorrectTag{Expected: expectedTag, Given: tag}
49 | }
50 | h := d.unsigned(int(value))
51 | *high = int32(h)
52 |
53 | return d.Error()
54 | }
55 |
--------------------------------------------------------------------------------
/deploy.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # 定义需要打包的程序目录
4 | program_dir="_output"
5 | app_name="driver-box"
6 | VERSION="v1.0.0"
7 | # 执行交叉编译
8 | rm -rf ${program_dir}
9 | export GOPROXY=https://mirrors.aliyun.com/goproxy/,direct
10 | go mod tidy
11 | go mod vendor
12 |
13 | build(){
14 | GOOS=$1
15 | GOARCH=$2
16 | output_flag="${program_dir}/${app_name}-${GOOS}-${GOARCH}"
17 | if [ "$GOOS" = "windows" ]; then
18 | output_flag="${program_dir}/${app_name}-${GOOS}-${GOARCH}.exe"
19 | fi
20 |
21 | GOOS=$GOOS GOARCH=${GOARCH} go build -o ${output_flag} main.go
22 | # 添加错误处理,确保在构建失败时脚本能够退出
23 | if [ $? -ne 0 ]; then
24 | echo "构建失败: ${output_flag}"
25 | exit 1
26 | fi
27 |
28 | echo "成功构建: ${output_flag}"
29 | }
30 | #make build VERSION=${VERSION} BuildTime=$(date +%Y%m%d%H%M%S)
31 | build linux arm64
32 | #build linux amd64
33 | #build linux arm
34 |
35 | #build windows amd64
36 | #build windows arm64
37 | #build darwin amd64
38 | #build darwin arm64
39 |
40 |
41 | # 遍历程序目录下的所有文件和文件夹
42 | for file in $(ls $program_dir)
43 | do
44 | deploy_file=''
45 | rm -rf driver-box
46 | mkdir driver-box
47 | cp -R res driver-box
48 |
49 | result=$(echo ${file} | grep "windows")
50 | if [[ "${result}" != "" ]]; then
51 | mv "${program_dir}/$file" driver-box/driver-box.exe
52 | # 如果是文件,则直接打包成tar包
53 | deploy_file="${file%.*}-${VERSION}.zip"
54 | zip -r ${deploy_file} driver-box
55 | else
56 | mv "${program_dir}/$file" driver-box/driver-box
57 | # 如果是文件,则直接打包成tar包
58 | deploy_file="${file}-${VERSION}.tar.gz"
59 | tar -czf ${deploy_file} driver-box
60 | fi
61 | mv ${deploy_file} ${program_dir}/
62 | done
63 | rm -rf driver-box
--------------------------------------------------------------------------------
/driverbox/restful/restful.go:
--------------------------------------------------------------------------------
1 | package restful
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/ibuilding-x/driver-box/driverbox/restful/response"
6 | "github.com/ibuilding-x/driver-box/internal/logger"
7 | "github.com/julienschmidt/httprouter"
8 | "go.uber.org/zap"
9 | "net/http"
10 | )
11 |
12 | var HttpRouter = httprouter.New()
13 |
14 | // Handler 处理函数
15 | type Handler func(*http.Request) (any, error)
16 |
17 | // HandleFunc 注册处理函数
18 | func HandleFunc(method, pattern string, handler Handler) {
19 | logger.Logger.Info("register api", zap.String("method", method), zap.String("pattern", pattern))
20 | HttpRouter.HandlerFunc(method, pattern, func(writer http.ResponseWriter, request *http.Request) {
21 | // 定义响应数据结构
22 | var data response.Common
23 |
24 | // 处理请求
25 | result, err := handler(request)
26 | if err != nil {
27 | // 定义错误信息
28 | data.ErrorMsg = err.Error()
29 | // 定义错误码
30 | if code, ok := errorCodes[err]; ok {
31 | data.ErrorCode = code
32 | } else {
33 | data.ErrorCode = errorCodes[UndefinedErr]
34 | }
35 | } else {
36 | data.Success = true
37 | data.ErrorCode = 200
38 | data.Data = result
39 | }
40 |
41 | // 设置响应头
42 | writer.Header().Set("Content-Type", "application/json")
43 |
44 | // 序列化响应数据
45 | b, err := json.Marshal(data)
46 | if err != nil {
47 | logger.Logger.Error("[api] json marshal fail", zap.Error(err))
48 | http.Error(writer, err.Error(), http.StatusInternalServerError)
49 | return
50 | }
51 |
52 | // 写入响应数据
53 | _, err = writer.Write(b)
54 | if err != nil {
55 | logger.Logger.Error("[api] write response fail", zap.Error(err))
56 | http.Error(writer, err.Error(), http.StatusInternalServerError)
57 | return
58 | }
59 | })
60 | }
61 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/whoisrouter.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | import (
4 | "fmt"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
6 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes/ndpu"
7 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/encoding"
8 | )
9 |
10 | /*
11 | Is in beta
12 | */
13 |
14 | func (c *client) WhoIsRouterToNetwork() (resp *[]btypes.Address) {
15 | var err error
16 | dest := *c.dataLink.GetBroadcastAddress()
17 | enc := encoding.NewEncoder()
18 | npdu := &btypes.NPDU{
19 | Version: btypes.ProtocolVersion,
20 | Destination: &dest,
21 | Source: c.dataLink.GetMyAddress(),
22 | IsNetworkLayerMessage: true,
23 | NetworkLayerMessageType: ndpu.WhoIsRouterToNetwork,
24 | // We are not expecting a direct reply from a single destination
25 | ExpectingReply: false,
26 | Priority: btypes.Normal,
27 | HopCount: btypes.DefaultHopCount,
28 | }
29 | enc.NPDU(npdu)
30 | // Run in parallel
31 | errChan := make(chan error)
32 | broadcast := &SetBroadcastType{Set: true, BacFunc: btypes.BacFuncBroadcast}
33 | go func() {
34 | _, err = c.Send(dest, npdu, enc.Bytes(), broadcast)
35 | errChan <- err
36 | }()
37 | values, err := c.utsm.Subscribe(1, 65534) //65534 is the max number a network can be
38 | if err != nil {
39 | fmt.Println(`err`, err)
40 | }
41 | err = <-errChan
42 | if err != nil {
43 |
44 | }
45 | var list []btypes.Address
46 | for _, addresses := range values {
47 | r, ok := addresses.([]btypes.Address)
48 | if !ok {
49 | continue
50 | }
51 | for _, addr := range r {
52 | list = append(list, addr)
53 | }
54 | }
55 | return &list
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/mock.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "github.com/ibuilding-x/driver-box/driverbox/helper"
7 | "github.com/ibuilding-x/driver-box/driverbox/helper/utils"
8 | "github.com/ibuilding-x/driver-box/driverbox/plugin/callback"
9 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
10 | lua "github.com/yuin/gopher-lua"
11 | "go.uber.org/zap"
12 | )
13 |
14 | func mockRead(plugin *connector, L *lua.LState, data btypes.MultiplePropertyData) error {
15 | for _, object := range data.Objects {
16 | for deviceId, pointName := range object.Points {
17 | mockData, e := helper.CallLuaMethod(L, "mockRead", lua.LString(deviceId), lua.LString(pointName))
18 | if e != nil {
19 | helper.Logger.Error("mockRead error", zap.Error(e))
20 | }
21 | v, e := utils.Conv2Float64(mockData)
22 | if e != nil {
23 | helper.Logger.Error("mockRead error", zap.Error(e))
24 | continue
25 | }
26 | resp := map[string]interface{}{
27 | "deviceId": deviceId,
28 | "pointName": pointName,
29 | "value": v,
30 | }
31 | respJson, err := json.Marshal(resp)
32 | res, err := plugin.Decode(respJson)
33 | if err != nil {
34 | helper.Logger.Error("error bacnet callback", zap.Any("data", respJson), zap.Error(err))
35 | } else {
36 | callback.ExportTo(res)
37 | }
38 | }
39 | }
40 | return nil
41 | }
42 |
43 | func mockWrite(L *lua.LState, deviceId, pointName string, value interface{}) error {
44 | result, err := helper.CallLuaMethod(L, "mockWrite", lua.LString(deviceId), lua.LString(pointName), lua.LString(fmt.Sprint(value)))
45 | if err == nil {
46 | helper.Logger.Info("mockWrite result", zap.Any("result", result))
47 | }
48 | return err
49 | }
50 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/services/services_test.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | pprint "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/helpers/print"
8 | )
9 |
10 | func TestSupported(t *testing.T) {
11 |
12 | //Object to store name and supported values for sorting
13 | type supportedObject struct {
14 | Name string
15 | Supported bool
16 | }
17 |
18 | ss := Supported{}
19 | //Imported array goes here - change name & references
20 | arrayTest := []bool{false, false, false, false, false, false, false, false, false, false, false, false, true, false, true, true, false, false, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false, true, false, false, false, false, false, false}
21 |
22 | //Creating array of services map in the correct order & size
23 | var servicesSize = len(ss.ListAll())
24 | orderedArray := make([]supportedObject, servicesSize)
25 |
26 | //Sorting services map to array
27 | for supported := range ss.ListAll() {
28 | //Assigning supported value from bool array
29 | supported.Supported = arrayTest[supported.Index]
30 |
31 | //Adding objects to sorted array
32 | obj := new(supportedObject)
33 | obj.Name = supported.Name
34 | obj.Supported = supported.Supported
35 | orderedArray[supported.Index] = *obj
36 | }
37 | //Printing sorted array of objects
38 | for i, v := range orderedArray {
39 | var supportedStatus string
40 |
41 | if v.Supported == true {
42 | supportedStatus = "Supported"
43 | }
44 | if v.Supported == false {
45 | supportedStatus = "Not Supported"
46 | }
47 | fmt.Println(i, v.Name+":", supportedStatus)
48 | }
49 |
50 | pprint.PrintJOSN(orderedArray)
51 | }
52 |
--------------------------------------------------------------------------------
/internal/plugins/tcpserver/plugin.go:
--------------------------------------------------------------------------------
1 | package tcpserver
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/common"
5 | "github.com/ibuilding-x/driver-box/driverbox/config"
6 | "github.com/ibuilding-x/driver-box/driverbox/helper"
7 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
8 | lua "github.com/yuin/gopher-lua"
9 | "go.uber.org/zap"
10 | )
11 |
12 | const ProtocolName = "tcp_server"
13 |
14 | type Plugin struct {
15 | logger *zap.Logger
16 | config config.Config
17 | connPool []*connector
18 | ls *lua.LState
19 | }
20 |
21 | // Initialize 插件初始化
22 | func (p *Plugin) Initialize(logger *zap.Logger, c config.Config, ls *lua.LState) {
23 | p.logger = logger
24 | p.config = c
25 | p.ls = ls
26 |
27 | // 初始化连接池
28 | if err := p.initConnPool(); err != nil {
29 | logger.Error("init connector pool failed", zap.Error(err))
30 | }
31 |
32 | }
33 |
34 | // Connector 连接器
35 | func (p *Plugin) Connector(deviceSn string) (connector plugin.Connector, err error) {
36 | return nil, common.NotSupportGetConnector
37 | }
38 |
39 | // Destroy 销毁插件
40 | func (p *Plugin) Destroy() error {
41 | if p.ls != nil {
42 | helper.Close(p.ls)
43 | }
44 | return nil
45 | }
46 |
47 | // initConnPool 初始化连接池
48 | func (p *Plugin) initConnPool() (err error) {
49 | p.connPool = make([]*connector, 0)
50 | for key, _ := range p.config.Connections {
51 | var c connectorConfig
52 | if err = helper.Map2Struct(p.config.Connections[key], &c); err != nil {
53 | return
54 | }
55 | conn := &connector{
56 | config: c,
57 | plugin: p,
58 | scriptDir: p.config.Key,
59 | ls: p.ls,
60 | }
61 | if err = conn.startServer(); err != nil {
62 | return
63 | }
64 | p.connPool = append(p.connPool, conn)
65 | }
66 | return
67 | }
68 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/core/log.go:
--------------------------------------------------------------------------------
1 | package dlt645
2 |
3 | import (
4 | "os"
5 | "sync/atomic"
6 |
7 | "log"
8 | )
9 |
10 | // 内部调试实现
11 | type logger struct {
12 | provider LogProvider
13 | // has log output enabled,
14 | // 1: enable
15 | // 0: disable
16 | has uint32
17 | }
18 |
19 | // newLogger new logger with prefix
20 | func newLogger(prefix string) logger {
21 | return logger{
22 | provider: defaultLogger{log.New(os.Stdout, prefix, log.LstdFlags)},
23 | has: 0,
24 | }
25 | }
26 |
27 | // LogMode set enable or disable log output when you has set logger
28 | func (sf *logger) LogMode(enable bool) {
29 | if enable {
30 | atomic.StoreUint32(&sf.has, 1)
31 | } else {
32 | atomic.StoreUint32(&sf.has, 0)
33 | }
34 | }
35 |
36 | // SetLogProvider overwrite log provider
37 | func (sf *logger) SetLogProvider(p LogProvider) {
38 | if p != nil {
39 | sf.provider = p
40 | }
41 | }
42 |
43 | // Error Log ERROR level message.
44 | func (sf logger) Error(format string, v ...interface{}) {
45 | if atomic.LoadUint32(&sf.has) == 1 {
46 | sf.provider.Error(format, v...)
47 | }
48 | }
49 |
50 | // Debug Log DEBUG level message.
51 | func (sf logger) Debug(format string, v ...interface{}) {
52 | if atomic.LoadUint32(&sf.has) == 1 {
53 | sf.provider.Debug(format, v...)
54 | }
55 | }
56 |
57 | // default log
58 | type defaultLogger struct {
59 | *log.Logger
60 | }
61 |
62 | // check implement LogProvider interface
63 | var _ LogProvider = (*defaultLogger)(nil)
64 |
65 | // Error Log ERROR level message.
66 | func (sf defaultLogger) Error(format string, v ...interface{}) {
67 | sf.Printf("[E]: "+format, v...)
68 | }
69 |
70 | // Debug Log DEBUG level message.
71 | func (sf defaultLogger) Debug(format string, v ...interface{}) {
72 | sf.Printf("[D]: "+format, v...)
73 | }
74 |
--------------------------------------------------------------------------------
/driverbox/pkg/mbserver/modbus/slave.go:
--------------------------------------------------------------------------------
1 | package modbus
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | type HandlerFunc func(c *Context)
8 |
9 | type Slave struct {
10 | handler ServerHandler
11 | functions map[int]HandlerFunc
12 | mu sync.RWMutex
13 |
14 | coils *coilStorage
15 | discreteInputs *coilStorage
16 | holdingRegisters *registerStorage
17 | inputRegisters *registerStorage
18 | }
19 |
20 | func (s *Slave) Listen() error {
21 | return s.handler.Listen()
22 | }
23 |
24 | func (s *Slave) Close() error {
25 | return s.handler.Close()
26 | }
27 |
28 | func (s *Slave) HandleFuncCode(code int, f HandlerFunc) {
29 | s.mu.Lock()
30 | defer s.mu.Unlock()
31 |
32 | s.handler.HandleFunc(s.handleProtocolMessageFunc)
33 | s.functions[code] = f
34 | }
35 |
36 | func (s *Slave) handleProtocolMessageFunc(message *ProtocolMessage) {
37 | funcCodeFunc, ok := s.functions[int(message.FunctionCode)]
38 | if !ok {
39 | message.ErrorCode = 1
40 | _ = s.handler.Send(message)
41 | return
42 | }
43 |
44 | ctx := s.createContext(message)
45 | funcCodeFunc(ctx)
46 | }
47 |
48 | func (s *Slave) createContext(message *ProtocolMessage) *Context {
49 | return &Context{
50 | ProtocolMessage: message,
51 | serverHandler: s.handler,
52 | Coils: s.coils,
53 | DiscreteInputs: s.discreteInputs,
54 | HoldingRegisters: s.holdingRegisters,
55 | InputRegisters: s.inputRegisters,
56 | }
57 | }
58 |
59 | func NewSlave(handler ServerHandler) *Slave {
60 | return &Slave{
61 | handler: handler,
62 | functions: make(map[int]HandlerFunc),
63 | mu: sync.RWMutex{},
64 | coils: &coilStorage{},
65 | discreteInputs: &coilStorage{},
66 | holdingRegisters: ®isterStorage{},
67 | inputRegisters: ®isterStorage{},
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/internal/plugins/httpserver/plugin.go:
--------------------------------------------------------------------------------
1 | package httpserver
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/common"
5 | "github.com/ibuilding-x/driver-box/driverbox/config"
6 | "github.com/ibuilding-x/driver-box/driverbox/helper"
7 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
8 | lua "github.com/yuin/gopher-lua"
9 | "go.uber.org/zap"
10 | )
11 |
12 | const ProtocolName = "http_server"
13 |
14 | type Plugin struct {
15 | logger *zap.Logger // 日志记录器
16 | config config.Config // 核心配置
17 |
18 | connPool []*connector // 连接器
19 | ls *lua.LState // lua 虚拟机
20 | }
21 |
22 | func (p *Plugin) Initialize(logger *zap.Logger, c config.Config, ls *lua.LState) {
23 | p.logger = logger
24 | p.config = c
25 | p.ls = ls
26 |
27 | // 初始化连接池
28 | if err := p.initConnPool(); err != nil {
29 | logger.Error("init connector pool failed", zap.Error(err))
30 | }
31 |
32 | }
33 |
34 | // Connector 此协议不支持获取连接器
35 | func (p *Plugin) Connector(deviceSn string) (connector plugin.Connector, err error) {
36 | return nil, common.NotSupportGetConnector
37 | }
38 |
39 | func (p *Plugin) Destroy() error {
40 | if p.ls != nil {
41 | helper.Close(p.ls)
42 | }
43 | if len(p.connPool) > 0 {
44 | for i, _ := range p.connPool {
45 | if err := p.connPool[i].Release(); err != nil {
46 | return err
47 | }
48 | }
49 | }
50 | return nil
51 | }
52 |
53 | // initConnPool 初始化连接池
54 | func (p *Plugin) initConnPool() (err error) {
55 | for key, _ := range p.config.Connections {
56 | var c connectorConfig
57 | if err = helper.Map2Struct(p.config.Connections[key], &c); err != nil {
58 | return
59 | }
60 | conn := &connector{
61 | plugin: p,
62 | scriptDir: p.config.Key,
63 | ls: p.ls,
64 | }
65 | conn.startServer(c)
66 | p.connPool = append(p.connPool, conn)
67 | }
68 | return
69 | }
70 |
--------------------------------------------------------------------------------
/internal/export/basic/export.go:
--------------------------------------------------------------------------------
1 | package basic
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "sync"
7 |
8 | "github.com/ibuilding-x/driver-box/internal/core"
9 |
10 | "github.com/google/uuid"
11 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
12 | )
13 |
14 | var driverInstance *Export
15 | var once = &sync.Once{}
16 |
17 | // 设备自动发现插件
18 | type Export struct {
19 | discover *Discover
20 | ready bool
21 | }
22 |
23 | func (export *Export) Init() error {
24 | // 检查并生成唯一码文件
25 | const uniqueCodeFile = ".driverbox_serial_no"
26 | if _, err := os.Stat(uniqueCodeFile); err == nil {
27 | // 文件存在则读取内容
28 | content, err := os.ReadFile(uniqueCodeFile)
29 | if err != nil {
30 | return fmt.Errorf("failed to read unique code file: %v", err)
31 | }
32 | core.Metadata.SerialNo = string(content)
33 | } else if os.IsNotExist(err) {
34 | // 生成UUID作为唯一码
35 | uniqueCode := uuid.New().String()
36 | if err := os.WriteFile(uniqueCodeFile, []byte(uniqueCode), 0644); err != nil {
37 | return fmt.Errorf("failed to write unique code file: %v", err)
38 | }
39 | core.Metadata.SerialNo = uniqueCode
40 | }
41 |
42 | export.ready = true
43 | export.discover = NewDiscover()
44 | registerApi()
45 | go export.discover.udpDiscover()
46 | return nil
47 | }
48 |
49 | func (export *Export) Destroy() error {
50 | export.ready = false
51 | export.discover.stopDiscover()
52 | return nil
53 | }
54 | func NewExport() *Export {
55 | once.Do(func() {
56 | driverInstance = &Export{}
57 | })
58 | return driverInstance
59 | }
60 |
61 | // 点位变化触发场景联动
62 | func (export *Export) ExportTo(deviceData plugin.DeviceData) {
63 |
64 | }
65 |
66 | // 继承Export OnEvent接口
67 | func (export *Export) OnEvent(eventCode string, key string, eventValue interface{}) error {
68 | return nil
69 | }
70 |
71 | func (export *Export) IsReady() bool {
72 | return export.ready
73 | }
74 |
--------------------------------------------------------------------------------
/internal/export/ai/mcp/tools/driver_box.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
7 | "github.com/ibuilding-x/driver-box/internal/core"
8 | "github.com/mark3labs/mcp-go/mcp"
9 | )
10 |
11 | var WritePointsTool = mcp.NewTool("write_points",
12 | mcp.WithDescription("针对指定设备下发控制指令,用于修改设备点位的值。通过提供设备ID和需要修改的点位列表,可以实现对设备的远程控制。"),
13 | mcp.WithString("device_id", mcp.Required(), mcp.Description("设备唯一标识符,用于指定要控制的目标设备")),
14 | mcp.WithArray("points",
15 | mcp.Required(),
16 | mcp.Description("设备点位信息结构,定义了点位的名称和要设置的值"),
17 | mcp.Properties(map[string]any{
18 | "name": map[string]any{"type": "string", "description": "点位名称,必须与设备物模型中定义的点位名称一致"},
19 | "value": map[string]any{"type": "any", "description": "要设置的点位值,支持多种数据类型(字符串、数字、布尔值等),需与点位定义的类型一致"},
20 | }),
21 | ),
22 | )
23 |
24 | var WritePointsHandler = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
25 | // 获取设备ID
26 | deviceID, err := request.RequireString("device_id")
27 | if err != nil {
28 | return mcp.NewToolResultError("设备ID参数错误: " + err.Error()), nil
29 | }
30 |
31 | // 获取点位列表
32 | if request.GetArguments() == nil {
33 | return mcp.NewToolResultError("点位列表参数错误"), nil
34 | }
35 | args := request.GetArguments()["points"]
36 |
37 | // 转换为PointData结构
38 | points := make([]plugin.PointData, 0)
39 | for _, v := range args.([]any) {
40 | v := v.(map[string]any)
41 | points = append(points, plugin.PointData{
42 | PointName: v["name"].(string),
43 | Value: v["value"],
44 | })
45 | }
46 |
47 | // 执行点位写入
48 | err = core.SendBatchWrite(deviceID, points)
49 | if err != nil {
50 | return mcp.NewToolResultError("写入点位失败: " + err.Error()), err
51 | }
52 |
53 | return mcp.NewToolResultText("成功写入 " + deviceID + " 设备的 " + fmt.Sprintf("%d", len(points)) + " 个点位"), nil
54 | }
55 |
--------------------------------------------------------------------------------
/internal/export/ai/mcp_client.go:
--------------------------------------------------------------------------------
1 | package ai
2 |
3 | import (
4 | langchaingo_mcp_adapter "github.com/i2y/langchaingo-mcp-adapter"
5 | "github.com/ibuilding-x/driver-box/driverbox/helper"
6 | "github.com/mark3labs/mcp-go/client"
7 | "github.com/mark3labs/mcp-go/mcp"
8 | "github.com/tmc/langchaingo/tools"
9 | "go.uber.org/zap"
10 | )
11 |
12 | func (export *Export) getTools() ([]mcp.Tool, error) {
13 | // sse client
14 | cli, err := client.NewStreamableHttpClient("http://localhost:8999/mcp")
15 | // sse client needs to manually start asynchronous communication
16 | // while stdio does not require it.
17 | err = cli.Start(export.ctx)
18 |
19 | initRequest := mcp.InitializeRequest{}
20 | initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
21 | initRequest.Params.ClientInfo = mcp.Implementation{
22 | Name: "driver-box-client",
23 | Version: "1.0.0",
24 | }
25 | initRequest.Params.Capabilities = mcp.ClientCapabilities{}
26 |
27 | _, err = cli.Initialize(export.ctx, initRequest)
28 | if err != nil {
29 | helper.Logger.Error("initialize error", zap.Error(err))
30 | }
31 | r, e := cli.ListTools(export.ctx, mcp.ListToolsRequest{})
32 | if e != nil {
33 | return nil, e
34 | }
35 | return r.Tools, nil
36 |
37 | }
38 |
39 | func (export *Export) getLangChainTools() ([]tools.Tool, error) {
40 | // Create an MCP client using stdio
41 | // sse client
42 | cli, err := client.NewSSEMCPClient("http://localhost:8999/sse")
43 | // sse client needs to manually start asynchronous communication
44 | // while stdio does not require it.
45 | err = cli.Start(export.ctx)
46 | //defer cli.Close()
47 |
48 | // Create the adapter
49 | adapter, err := langchaingo_mcp_adapter.New(cli)
50 | if err != nil {
51 | helper.Logger.Error("Failed to create adapter", zap.Error(err))
52 | return nil, err
53 | }
54 |
55 | // Get all tools from MCP server
56 | return adapter.Tools()
57 | }
58 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/cmd/cmd/whois_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet"
6 | pprint "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/helpers/print"
7 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/network"
8 | "testing"
9 | )
10 |
11 | func TestByInterface(t *testing.T) {
12 | Interface = "VMware Network Adapter VMnet0"
13 | Port = 12345
14 | deviceIP = "192.168.9.19"
15 | client, err := network.New(&network.Network{Interface: Interface, Port: Port})
16 | if err != nil {
17 | fmt.Println("ERR-client", err)
18 | return
19 | }
20 | defer client.NetworkClose()
21 | go client.NetworkRun()
22 |
23 | _, err2 := network.New(&network.Network{Ip: "192.168.9.9", SubnetCIDR: 24, Port: Port})
24 | if err2 != nil {
25 | fmt.Println("ERR-client2", err2)
26 | }
27 |
28 | wi := &bacnet.WhoIsOpts{
29 | High: -1,
30 | Low: -1,
31 | GlobalBroadcast: true,
32 | NetworkNumber: 0,
33 | }
34 | pprint.PrintJOSN(wi)
35 |
36 | whoIs, err := client.Whois(wi)
37 | if err != nil {
38 | fmt.Println("ERR-whoIs", err)
39 | return
40 | }
41 | pprint.PrintJOSN(whoIs)
42 | }
43 |
44 | func TestByIp(t *testing.T) {
45 | // Interface = "VMware Network Adapter VMnet0"
46 | Port = 47808
47 | client, err := network.New(&network.Network{Ip: "192.168.9.9", SubnetCIDR: 24, Port: Port})
48 | if err != nil {
49 | fmt.Println("ERR-client", err)
50 | return
51 | }
52 | defer client.NetworkClose()
53 | go client.NetworkRun()
54 |
55 | wi := &bacnet.WhoIsOpts{
56 | High: -1,
57 | Low: -1,
58 | GlobalBroadcast: true,
59 | NetworkNumber: 0,
60 | }
61 | pprint.PrintJOSN(wi)
62 |
63 | whoIs, err := client.Whois(wi)
64 | if err != nil {
65 | fmt.Println("ERR-whoIs", err)
66 | return
67 | }
68 | pprint.PrintJOSN(whoIs)
69 | }
70 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/network/device.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "fmt"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet"
6 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
7 | )
8 |
9 | type Device struct {
10 | Ip string
11 | Port int
12 | DeviceID int
13 | NetworkNumber int
14 | MacMSTP int
15 | MaxApdu uint32
16 | Segmentation uint32
17 | StoreID string
18 | dev btypes.Device
19 | network bacnet.Client
20 | }
21 |
22 | // NewDevice returns a new instance of ta bacnet device
23 | func NewDevice(net *Network, device *Device) (*Device, error) {
24 | var err error
25 | if net == nil {
26 | fmt.Println("network can not be nil")
27 | return nil, err
28 | }
29 | dev := &btypes.Device{
30 | Ip: device.Ip,
31 | DeviceID: device.DeviceID,
32 | NetworkNumber: device.NetworkNumber,
33 | MacMSTP: device.MacMSTP,
34 | MaxApdu: device.MaxApdu,
35 | Segmentation: btypes.Enumerated(device.Segmentation),
36 | }
37 | dev, err = btypes.NewDevice(dev)
38 | if err != nil {
39 | return nil, err
40 | }
41 | if dev == nil {
42 | fmt.Println("dev is nil")
43 | return nil, err
44 | }
45 | device.network = net.Client
46 | device.dev = *dev
47 | if BacStore != nil {
48 | BacStore.Set(device.StoreID, device, -1)
49 | }
50 | return device, nil
51 | }
52 |
53 | // update attributes to internal btypes.Device
54 | func (dev *Device) Update() error {
55 | bdev := &btypes.Device{
56 | Ip: dev.Ip,
57 | DeviceID: dev.DeviceID,
58 | NetworkNumber: dev.NetworkNumber,
59 | MacMSTP: dev.MacMSTP,
60 | MaxApdu: dev.MaxApdu,
61 | Segmentation: btypes.Enumerated(dev.Segmentation),
62 | }
63 | bdev, err := btypes.NewDevice(bdev)
64 | if err != nil {
65 | return err
66 | }
67 | dev.dev = *bdev
68 | return nil
69 | }
70 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/whatnetwork.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | import (
4 | "fmt"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
6 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes/ndpu"
7 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/encoding"
8 | )
9 |
10 | /*
11 | Is in beta, works but needs a decoder
12 |
13 | in bacnet.Send() need to set the header.Function as btypes.BacFuncBroadcast
14 |
15 | in bacnet.handleMsg() the npdu.IsNetworkLayerMessage is always rejected so this needs to be updated
16 |
17 | */
18 |
19 | func (c *client) WhatIsNetworkNumber() (resp []*btypes.Address) {
20 | var err error
21 | dest := *c.dataLink.GetBroadcastAddress()
22 | enc := encoding.NewEncoder()
23 | npdu := &btypes.NPDU{
24 | Version: btypes.ProtocolVersion,
25 | Destination: &dest,
26 | Source: c.dataLink.GetMyAddress(),
27 | IsNetworkLayerMessage: true,
28 | NetworkLayerMessageType: ndpu.WhatIsNetworkNumber,
29 | // We are not expecting a direct reply from a single destination
30 | ExpectingReply: false,
31 | Priority: btypes.Normal,
32 | HopCount: btypes.DefaultHopCount,
33 | }
34 | enc.NPDU(npdu)
35 | // Run in parallel
36 | errChan := make(chan error)
37 | broadcast := &SetBroadcastType{Set: true, BacFunc: btypes.BacFuncBroadcast}
38 | go func() {
39 | _, err = c.Send(dest, npdu, enc.Bytes(), broadcast)
40 | errChan <- err
41 | }()
42 | values, err := c.utsm.Subscribe(1, 65534) //65534 is the max number a network can be
43 | if err != nil {
44 | fmt.Println(`err`, err)
45 | }
46 | err = <-errChan
47 | if err != nil {
48 |
49 | }
50 |
51 | for _, v := range values {
52 | r, ok := v.(btypes.NPDU)
53 | if r.Source != nil {
54 | resp = append(resp, r.Source)
55 | }
56 | if !ok {
57 | continue
58 | }
59 | }
60 | return resp
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/driverbox/library/tag.go:
--------------------------------------------------------------------------------
1 | package library
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/ibuilding-x/driver-box/driverbox/config"
6 | "golang.org/x/exp/slices"
7 | "os"
8 | "path"
9 | "sync"
10 | )
11 |
12 | const (
13 | tagDir = "tag"
14 | tagFile = "tag.json"
15 | )
16 |
17 | var UsageTag = &usageTag{
18 | language: "zh-CN",
19 | }
20 |
21 | type Tag struct {
22 | // Key 唯一标识
23 | Key string `json:"key"`
24 | // Desc 标签描述(国际化)
25 | Desc map[string]string `json:"desc"`
26 | }
27 |
28 | func (t Tag) GetDesc(lang ...string) string {
29 | if len(lang) > 0 {
30 | return t.Desc[lang[0]]
31 | }
32 |
33 | return t.Desc[UsageTag.language]
34 | }
35 |
36 | type usageTag struct {
37 | language string
38 | cacheTags []Tag
39 | lock sync.Mutex
40 | }
41 |
42 | func (ut *usageTag) SetLanguage(lang string) {
43 | if lang != "" {
44 | ut.language = lang
45 | }
46 | }
47 |
48 | func (ut *usageTag) All() []Tag {
49 | ut.lock.Lock()
50 | defer ut.lock.Unlock()
51 |
52 | if ut.cacheTags == nil {
53 | filePath := path.Join(config.ResourcePath, baseDir, tagDir, tagFile)
54 | if bs, err := os.ReadFile(filePath); err == nil {
55 | _ = json.Unmarshal(bs, &ut.cacheTags)
56 | }
57 | }
58 |
59 | if len(ut.cacheTags) == 0 {
60 | return nil
61 | }
62 |
63 | result := make([]Tag, len(ut.cacheTags))
64 | copy(result, ut.cacheTags)
65 | return result
66 | }
67 |
68 | func (ut *usageTag) Get(key string) (Tag, bool) {
69 | tags := ut.All()
70 | for _, tag := range tags {
71 | if tag.Key == key {
72 | return tag, true
73 | }
74 | }
75 | return Tag{}, false
76 | }
77 |
78 | func (ut *usageTag) Filter(filter []string) []Tag {
79 | tags := ut.All()
80 | if len(filter) == 0 {
81 | return tags
82 | }
83 |
84 | result := make([]Tag, 0, len(tags))
85 | for _, tag := range tags {
86 | if slices.Contains(filter, tag.Key) {
87 | result = append(result, tag)
88 | }
89 | }
90 | return result
91 | }
92 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/plugin.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/common"
5 | "github.com/ibuilding-x/driver-box/driverbox/config"
6 | "github.com/ibuilding-x/driver-box/driverbox/helper"
7 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
8 | lua "github.com/yuin/gopher-lua"
9 | "go.uber.org/zap"
10 | )
11 |
12 | const ProtocolName = "bacnet"
13 |
14 | type Plugin struct {
15 | logger *zap.Logger
16 | config config.Config
17 | connPool map[string]plugin.Connector
18 | ls *lua.LState
19 | }
20 |
21 | // Initialize 插件初始化
22 | // logger *zap.Logger、ls *lua.LState 参数未来可能会废弃
23 | func (p *Plugin) Initialize(logger *zap.Logger, c config.Config, ls *lua.LState) {
24 | p.logger = logger
25 | p.config = c
26 | p.ls = ls
27 |
28 | // 初始化连接
29 | if err := p.initNetworks(); err != nil {
30 | logger.Error("initialize bacnet plugin error", zap.Error(err))
31 | }
32 |
33 | }
34 |
35 | // Connector 连接器
36 | func (p *Plugin) Connector(deviceName string) (connector plugin.Connector, err error) {
37 | if device, ok := helper.CoreCache.GetDevice(deviceName); ok {
38 | if conn, ok := p.connPool[device.ConnectionKey]; ok {
39 | return conn, nil
40 | }
41 | return nil, common.ConnectorNotFound
42 | }
43 | return nil, common.DeviceNotFoundError
44 | }
45 |
46 | // Destroy 销毁插件
47 | func (p *Plugin) Destroy() error {
48 | for _, conn := range p.connPool {
49 | c := conn.(*connector)
50 | c.Close()
51 | }
52 | if p.ls != nil {
53 | helper.Close(p.ls)
54 | }
55 | return nil
56 | }
57 |
58 | // initNetworks 初始化连接池
59 | func (p *Plugin) initNetworks() (err error) {
60 | p.connPool = make(map[string]plugin.Connector)
61 | for connName, conn := range p.config.Connections {
62 | if n, err := initConnector(connName, conn.(map[string]interface{}), p); err == nil {
63 | p.connPool[connName] = n
64 | } else {
65 | return err
66 | }
67 | }
68 | return nil
69 | }
70 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/adapter.go:
--------------------------------------------------------------------------------
1 | package dlt645
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/ibuilding-x/driver-box/driverbox/helper"
7 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
8 | )
9 |
10 | // Decode 解码数据
11 | func (c *connector) Decode(raw interface{}) (res []plugin.DeviceData, err error) {
12 | readValue, ok := raw.(plugin.PointReadValue)
13 | if !ok {
14 | return nil, fmt.Errorf("unexpected raw: %v", raw)
15 | }
16 |
17 | res = append(res, plugin.DeviceData{
18 | ID: readValue.ID,
19 | Values: []plugin.PointData{{
20 | PointName: readValue.PointName,
21 | Value: readValue.Value,
22 | }},
23 | })
24 | return
25 | }
26 |
27 | // Encode 编码数据
28 | func (c *connector) Encode(deviceId string, mode plugin.EncodeMode, values ...plugin.PointData) (res interface{}, err error) {
29 | if mode == plugin.WriteMode {
30 | return nil, err
31 | }
32 |
33 | device, ok := helper.CoreCache.GetDevice(deviceId)
34 | if !ok {
35 | return nil, fmt.Errorf("device [%s] not found", deviceId)
36 | }
37 | unitId, e := getMeterAddress(device.Properties)
38 | if e != nil {
39 | return nil, e
40 | }
41 | slave := c.devices[unitId]
42 | if slave == nil {
43 | return nil, fmt.Errorf("device [%s] not found", deviceId)
44 | }
45 |
46 | indexes := make(map[int]*pointGroup)
47 | var pointGroups []*pointGroup
48 | //寻找待读点位关联的pointGroup
49 | for _, readPoint := range values {
50 | ok = false
51 | for _, group := range slave.pointGroup {
52 | for _, point := range group.Points {
53 | if point.Name() == readPoint.PointName {
54 | if _, ok := indexes[group.index]; !ok {
55 | indexes[group.index] = group
56 | pointGroups = append(pointGroups, group)
57 | }
58 | ok = true
59 | break
60 | }
61 | }
62 | //匹配成功
63 | if ok {
64 | break
65 | }
66 | }
67 | }
68 |
69 | //找到待读点所属的group
70 | return command{
71 | Mode: BatchReadMode,
72 | Value: pointGroups,
73 | }, nil
74 | }
75 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/network/strings.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
6 | )
7 |
8 | // ReadString to read a string like objectName
9 | func (device *Device) ReadString(obj *Object) (string, error) {
10 | read, err := device.Read(obj)
11 | if err != nil {
12 | return "", err
13 | }
14 | return device.toStr(read), nil
15 | }
16 |
17 | func (device *Device) ReadDeviceName(ObjectID btypes.ObjectInstance) (string, error) {
18 | obj := &Object{
19 | ObjectID: ObjectID,
20 | ObjectType: btypes.DeviceType,
21 | Prop: btypes.PropObjectName,
22 | ArrayIndex: bacnet.ArrayAll,
23 | }
24 | read, err := device.Read(obj)
25 | if err != nil {
26 | return "", err
27 | }
28 | return device.toStr(read), nil
29 | }
30 |
31 | func (device *Device) WriteDeviceName(ObjectID btypes.ObjectInstance, value string) error {
32 | write := &Write{
33 | ObjectID: ObjectID,
34 | ObjectType: btypes.DeviceType,
35 | Prop: btypes.PropObjectName,
36 | WriteValue: value,
37 | }
38 | err := device.Write(write)
39 | if err != nil {
40 | return err
41 | }
42 | return nil
43 | }
44 |
45 | func (device *Device) ReadPointName(pnt *Point) (string, error) {
46 | obj := &Object{
47 | ObjectID: pnt.ObjectID,
48 | ObjectType: pnt.ObjectType,
49 | Prop: btypes.PropObjectName,
50 | ArrayIndex: bacnet.ArrayAll,
51 | }
52 | read, err := device.Read(obj)
53 | if err != nil {
54 | return "", err
55 | }
56 | return device.toStr(read), nil
57 | }
58 |
59 | func (device *Device) WritePointName(pnt *Point, value string) error {
60 | write := &Write{
61 | ObjectID: pnt.ObjectID,
62 | ObjectType: pnt.ObjectType,
63 | Prop: btypes.PropObjectName,
64 | WriteValue: value,
65 | }
66 | err := device.Write(write)
67 | if err != nil {
68 | return err
69 | }
70 |
71 | return nil
72 | }
73 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/model.go:
--------------------------------------------------------------------------------
1 | package dlt645
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/ibuilding-x/driver-box/driverbox/config"
7 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
8 | )
9 |
10 | type primaryTable string
11 |
12 | const BatchReadMode plugin.EncodeMode = "batchRead"
13 |
14 | // ConnectionConfig 连接器配置
15 | type ConnectionConfig struct {
16 | plugin.BaseConnection
17 | Address string `json:"address"` // 地址:例如:127.0.0.1:502
18 | BaudRate uint `json:"baudRate"` // 波特率(仅串口模式)
19 | DataBits uint `json:"dataBits"` // 数据位(仅串口模式)
20 | StopBits uint `json:"stopBits"` // 停止位(仅串口模式)
21 | Parity string `json:"parity"` // 奇偶性校验(仅串口模式)
22 | MinInterval uint16 `json:"minInterval"` // 最小读取间隔
23 | Timeout uint16 `json:"timeout"` // 请求超时
24 | Retry int `json:"retry"` // 重试次数
25 | AutoReconnect bool `json:"autoReconnect"` //自动重连
26 | ProtocolLogEnabled bool `json:"protocolLogEnabled"` // 协议解析日志
27 | }
28 |
29 | // Point 点位
30 | type Point struct {
31 | config.Point
32 | //冗余设备相关信息
33 | DeviceId string
34 |
35 | //点位采集周期
36 | Duration string `json:"duration"`
37 | Address uint16
38 | Quantity uint16 `json:"quantity"`
39 | DataMaker string `json:"dataMaker"`
40 | }
41 |
42 | // 采集组
43 | type slaveDevice struct {
44 | // 通讯设备,采集点位可以对应多个物模型设备
45 | address string
46 | //分组
47 | pointGroup []*pointGroup
48 | }
49 |
50 | type pointGroup struct {
51 | index int //分组索引
52 | Duration time.Duration //采集间隔
53 | LatestTime time.Time //上一次采集时间
54 | Address string //起始地址
55 | Quantity uint16 //数量
56 | Points []*Point
57 | DataMaker string // dlt645标准中点位标识
58 | SlaveId string // 电表地址
59 | }
60 |
61 | // Connector#Send接入入参
62 | type command struct {
63 | Mode plugin.EncodeMode // 模式
64 | Value interface{}
65 | }
66 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/btypes/address.go:
--------------------------------------------------------------------------------
1 | package btypes
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | )
7 |
8 | type Address struct {
9 | Net uint16 // BACnet network number
10 | Len uint8
11 | MacLen uint8 // mac len 0 is a broadcast address
12 | Mac []uint8 //note: MAC for IP addresses uses 4 bytes for addr, 2 bytes for port
13 | Adr []uint8 // hardware addr (MAC) address of ms-tp devices
14 | Id uint16
15 | }
16 |
17 | const GlobalBroadcast uint16 = 0xFFFF
18 | const broadcastNetwork uint16 = 0xFFFF
19 |
20 | // IsBroadcast returns if the address is a broadcast address
21 | func (a *Address) IsBroadcast() bool {
22 | //qygeng
23 | // if a.Net == broadcastNetwork || a.MacLen == 0 {
24 | // return true
25 | // }
26 | return a.Net == broadcastNetwork
27 | }
28 |
29 | // SetLength if device is of type ms-tp then set address len to 1
30 | func (a *Address) SetLength() {
31 | if len(a.Adr) > 0 {
32 | a.Len = 1
33 | } else {
34 | //qygeng:fix bug
35 | a.Len = uint8(len(a.Mac))
36 | }
37 | }
38 |
39 | func (a *Address) SetBroadcast(b bool) {
40 | if b {
41 | a.MacLen = 0
42 | } else {
43 | a.MacLen = uint8(len(a.Mac))
44 | }
45 | }
46 |
47 | // IsSubBroadcast checks to see if packet is meant to be a network
48 | // specific broadcast
49 | func (a *Address) IsSubBroadcast() bool {
50 | if a.Net > 0 && a.Len == 0 {
51 | return true
52 | }
53 | return false
54 | }
55 |
56 | // IsUnicast checks to see if packet is meant to be a unicast
57 | func (a *Address) IsUnicast() bool {
58 | return a.MacLen == 6
59 | }
60 |
61 | // UDPAddr parses the mac address and returns a proper net.UDPAddr
62 | func (a *Address) UDPAddr() (net.UDPAddr, error) {
63 | if len(a.Mac) != 6 {
64 | return net.UDPAddr{}, fmt.Errorf("mac is too short at %d", len(a.Mac))
65 | }
66 | port := uint(a.Mac[4])<<8 | uint(a.Mac[5])
67 | ip := net.IPv4(a.Mac[0], a.Mac[1], a.Mac[2], a.Mac[3])
68 | return net.UDPAddr{
69 | IP: ip,
70 | Port: int(port),
71 | }, nil
72 | }
73 |
--------------------------------------------------------------------------------
/internal/dto/ws.go:
--------------------------------------------------------------------------------
1 | package dto
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/config"
5 | "github.com/ibuilding-x/driver-box/driverbox/pkg/shadow"
6 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
7 | )
8 |
9 | type WSPayloadType int8
10 |
11 | const (
12 | WSForRegister WSPayloadType = iota + 1 // 注册请求
13 | WSForRegisterRes // 注册响应
14 | WSForUnregister // 取消注册请求
15 | WSForUnregisterRes // 取消注册成功响应
16 | WSForPing // 心跳
17 | WSForPong // 心跳响应
18 | WSForReport // 上报请求
19 | WSForReportRes // 上报响应
20 | WSForControl // 控制请求
21 | WSForControlRes // 控制响应
22 | WSForSyncModels // 同步模型请求
23 | WSForSyncModelsRes // 同步模型响应
24 | WSForSyncDevices // 同步设备请求
25 | WSForSyncDevicesRes // 同步设备响应
26 | WSForSyncShadow // 同步设备影子请求
27 | WSForSyncShadowRes // 同步设备影子响应
28 | )
29 |
30 | // WSPayload websocket 消息体
31 | type WSPayload struct {
32 | Type WSPayloadType `json:"type"` // 消息类型
33 | GatewayKey string `json:"gateway_key"` // 网关唯一标识(当前版本使用主网关的连接 Key),当 type 为 WSForRegister、 WSForUnregister 时,此字段必填
34 | DeviceData plugin.DeviceData `json:"device_data"` // 当 type 为 WSForReport、 WSForControl 时,此字段必填
35 | Models []config.DeviceModel `json:"models"` // 模型数据,当 type 为 WSForSyncModels 时,此字段必填
36 | Devices []config.Device `json:"devices"` // 设备数据,当 type 为 WSForSyncDevices 时,此字段必填
37 | Shadow []shadow.Device `json:"shadow"` // 设别影子数据,当 type 为 WSForSyncShadow 时,此字段必填
38 | Error string `json:"error"` // 错误信息,当 type 为 WSForRegisterRes、 WSForUnregisterRes、 WSForControlRes 时,此字段必填
39 | }
40 |
--------------------------------------------------------------------------------
/driverbox/helper/crontab/crontab.go:
--------------------------------------------------------------------------------
1 | package crontab
2 |
3 | import (
4 | "sync"
5 | "time"
6 |
7 | "github.com/robfig/cron/v3"
8 | )
9 |
10 | var instance *crontab
11 | var once = &sync.Once{}
12 |
13 | type Crontab interface {
14 | Clear()
15 | // AddFunc s please refer to time.ParseDuration
16 | AddFunc(s string, f func()) (*Future, error)
17 | }
18 |
19 | func Instance() Crontab {
20 | once.Do(func() {
21 | instance = &crontab{
22 | c: cron.New(cron.WithSeconds()),
23 | }
24 | instance.c.Start()
25 | })
26 | return instance
27 | }
28 |
29 | type Future struct {
30 | //外部传入的定时任务函数
31 | function func()
32 | //定时器
33 | ticker *time.Ticker
34 | cronId cron.EntryID
35 | //是否启用
36 | enable bool
37 | }
38 | type crontab struct {
39 | futures []*Future
40 | c *cron.Cron
41 | }
42 |
43 | func (c *crontab) Clear() {
44 | if len(c.futures) > 0 {
45 | for i, _ := range c.futures {
46 | c.futures[i].Disable()
47 | }
48 | c.futures = make([]*Future, 0)
49 | }
50 | }
51 |
52 | func (c *crontab) AddFunc(s string, f func()) (*Future, error) {
53 | d, err := time.ParseDuration(s)
54 | if err == nil {
55 | function := &Future{
56 | function: f,
57 | ticker: time.NewTicker(d),
58 | enable: true,
59 | }
60 | c.futures = append(c.futures, function)
61 | go function.run()
62 | return function, nil
63 | }
64 | //尝试按照crontab格式添加
65 | cronId, err := c.c.AddFunc(s, f)
66 | if err != nil {
67 | return &Future{}, err
68 | }
69 | function := &Future{
70 | function: f,
71 | cronId: cronId,
72 | enable: true,
73 | }
74 | c.futures = append(c.futures, function)
75 | return function, nil
76 | }
77 |
78 | func (f *Future) run() {
79 | for range f.ticker.C {
80 | if !f.enable {
81 | f.ticker.Stop()
82 | break
83 | }
84 | f.function()
85 | }
86 | }
87 | func (f *Future) Disable() {
88 | if !f.enable {
89 | return
90 | }
91 | f.enable = false
92 | if f.ticker != nil {
93 | f.ticker.Reset(1)
94 | }
95 | if f.cronId != 0 {
96 | instance.c.Remove(f.cronId)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/driverbox/pkg/mbserver/modbus/storage.go:
--------------------------------------------------------------------------------
1 | package modbus
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "sync"
7 | )
8 |
9 | type coilStorage struct {
10 | v [65536]bool
11 | mu sync.RWMutex
12 | }
13 |
14 | func (c *coilStorage) Read(address uint16, quantity uint16) (values []bool, err error) {
15 | c.mu.RLock()
16 | defer c.mu.RUnlock()
17 |
18 | if err = verifyAddressQuantity(address, quantity); err != nil {
19 | return
20 | }
21 |
22 | values = make([]bool, quantity)
23 | copy(values, c.v[address:address+quantity])
24 | return
25 | }
26 |
27 | func (c *coilStorage) Write(address uint16, values []bool) (err error) {
28 | c.mu.Lock()
29 | defer c.mu.Unlock()
30 |
31 | quantity := uint16(len(values))
32 | if err = verifyAddressQuantity(address, quantity); err != nil {
33 | return
34 | }
35 |
36 | copy(c.v[address:address+quantity], values)
37 | return
38 | }
39 |
40 | type registerStorage struct {
41 | v [65536]uint16
42 | mu sync.RWMutex
43 | }
44 |
45 | func (r *registerStorage) Read(address uint16, quantity uint16) (values []uint16, err error) {
46 | r.mu.RLock()
47 | defer r.mu.RUnlock()
48 |
49 | if err = verifyAddressQuantity(address, quantity); err != nil {
50 | return
51 | }
52 |
53 | values = make([]uint16, quantity)
54 | copy(values, r.v[address:address+quantity])
55 | return
56 | }
57 |
58 | func (r *registerStorage) Write(address uint16, values []uint16) (err error) {
59 | r.mu.Lock()
60 | defer r.mu.Unlock()
61 |
62 | quantity := uint16(len(values))
63 | if err = verifyAddressQuantity(address, quantity); err != nil {
64 | return
65 | }
66 |
67 | copy(r.v[address:address+quantity], values)
68 | return
69 | }
70 |
71 | func verifyAddressQuantity(address uint16, quantity uint16) error {
72 | if quantity < 1 || quantity > 2000 {
73 | return fmt.Errorf("modbus: quantity '%d' must be between '%d' and '%d'", quantity, 1, 2000)
74 | }
75 |
76 | if int(address+quantity) > math.MaxUint16+1 {
77 | return fmt.Errorf("modbus: quantity '%d' is out of range", quantity)
78 | }
79 |
80 | return nil
81 | }
82 |
--------------------------------------------------------------------------------
/driverbox/plugin/model.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/driverbox/event"
5 | )
6 |
7 | // 触发 ExportTo 的类型
8 | type ExportType string
9 |
10 | // EncodeMode 编码模式
11 | type EncodeMode string
12 |
13 | const (
14 | ReadMode EncodeMode = "read" // 读模式
15 | WriteMode EncodeMode = "write" // 写模式
16 | RealTimeExport ExportType = "realTimeExport" //实时上报
17 | )
18 |
19 | // PointData 点位数据
20 | type PointData struct {
21 | PointName string `json:"name"` // 点位名称
22 | Value interface{} `json:"value"` // 点位值
23 | }
24 |
25 | // DeviceData 设备数据
26 | type DeviceData struct {
27 | ID string `json:"id"`
28 | Values []PointData `json:"values"`
29 | Events []event.Data `json:"events"`
30 | ExportType ExportType //上报类型,底层的变化上报和实时上报等同于RealTimeExport
31 | }
32 |
33 | // PointReadValue 点位读操作的结构体
34 | type PointReadValue struct {
35 | //设备 ID
36 | ID string `json:"id"`
37 | // PointName 点位名称
38 | PointName string `json:"pointName"`
39 | // Value 点位值
40 | Value interface{} `json:"value"`
41 | }
42 |
43 | // PointWriteValue 点位写操作的结构体
44 | type PointWriteValue struct {
45 | // PointName 点位名称
46 | PointName string `json:"pointName"`
47 | // Value 点位值
48 | Value interface{} `json:"value"`
49 | //模型名称,某些驱动解析需要根据模型作区分
50 | ModelName string `json:"modelName"`
51 | //前置操作,例如空开要先解锁,空调要先开机
52 | PreOp []PointWriteValue `json:"preOp"`
53 | }
54 |
55 | // 连接配置基础模型
56 | type BaseConnection struct {
57 | ConnectionKey string //连接标识
58 | //ScriptEnable bool //是否存在动态脚本
59 | ////当前连接的 lua 虚拟机
60 | //Ls *lua.LState
61 | ProtocolKey string `json:"protocolKey"` //协议驱动库标识
62 | Discover bool `json:"discover"` //是否支持设备发现
63 | Enable bool `json:"enable"` //是否启用
64 | Virtual bool `json:"virtual"` //虚拟设备功能
65 | }
66 |
67 | // 简单的编码结构体,对于无Encode实现的插件,可以使用该结构体
68 | type SimpleEncodeStruct struct {
69 | // 设备 ID
70 | ID string `json:"id"`
71 | // 点位名称
72 | Mode EncodeMode `json:"mode"`
73 | // 点位值
74 | Values []PointData `json:"values"`
75 | }
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DriverBox
2 | [](https://deepwiki.com/ibuilding-X/driver-box)
3 |
4 | ## **我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧**
5 | 投过票的有缘人进交流群前可向群主报备一下,后续提供专属的技术咨询服务。
6 |
7 |
8 | ## 文档
9 |
10 | [快速开始](https://ibuilding-x.github.io/driver-box/)
11 |
12 | ## 简介
13 | driver-box 是一款支持泛化协议接入的边缘网关框架, 以插件化的形式融合了 Modbus、Bacnet、HTTP、MQTT 等主流协议,同时也支持基于TCP的各类私有化协议对接。
14 | 
15 |
16 | 我们期望 driver-box 能够为相关人士提供更加高效、舒适的设备接入体验。
17 |
18 | 通过对各类设备的通信协议和数据交互形式进行抽象,定义了一套标准流程以涵盖各类通信协议的共性逻辑,并结合动态解析脚本(Lua)填补其中的差异化部分。
19 |
20 | 以此解决设备接入过程中存在的驱动工程数量爆炸;接入标准难以规范化等问题。
21 |
22 | ## 特性
23 | ### 免费开源
24 | 采用商业友好的 Apache-2.0 开源协议,使其成为 IoT 生态圈的极佳选择。
25 |
26 | ### 架构
27 | 以 Golang 为主要开发语言,可编译出适配 amd64、arm64、armv7、x86 等系统架构的可执行程序。 存储空间和运行内存控制在十几MB,满足低规格网关的运行需求。
28 |
29 | 采用高度统一的配置化方式对接各类通讯设备。理想情况下只需编写一个 JSON 文件便可完成设备接入,亦可结合 lua 脚本实现复杂设备的数据加工。
30 |
31 | 通过精心的架构设计,三方用户可无限扩展边缘网关的设备通讯能力和应用服务能力。
32 |
33 | ### API
34 | driver-box 没有提供配套的 UI 界面,但开放了大量实用 RestAPI。用户可以自由设计网关 UI,定制出极致用户体验的边缘产品。
35 |
36 | ### 应用场景
37 | driver-box 适用于多种场景,包括智能家居、智慧楼宇、智慧工厂、智慧门店。它促进了设备数据的采集与场景融合,实现了万物皆可连、万物皆可互联、万物皆可智联。
38 |
39 | ## 安装
40 |
41 | 1. 下载源代码
42 |
43 | ```bash
44 | git clone https://gitee.com/iBUILDING-X/driver-box.git
45 | ```
46 |
47 | 2. 加载 go 依赖
48 |
49 | ```bash
50 | cd driver-box
51 | go mod vendor # 国内用户可以切换源:go env -w GOPROXY=https://goproxy.cn,direct
52 | ```
53 |
54 | ## 本地运行
55 |
56 | 1. 打开 main.go 文件
57 |
58 | ```go
59 | func main() {
60 | driverbox.Start([]export.Export{&export.DefaultExport{}})
61 | select {}
62 | }
63 | ```
64 |
65 | 2. 启动 driver-box
66 |
67 | ```bash
68 | go run main.go
69 | ```
70 |
71 | ## 参与贡献
72 |
73 | 1. Fork 本仓库
74 | 2. 新建 Feat_xxx 分支
75 | 3. 提交代码
76 | 4. 新建 Pull Request
77 |
78 | ## 反馈
79 |
80 | 如果您有任何问题,请通过 [issues](https://gitee.com/iBUILDING-X/driver-box/issues) 快速反馈
81 |
82 | ## 致谢
83 |
84 | - [EdgeX Foundry](https://www.edgexfoundry.org/)
85 |
--------------------------------------------------------------------------------
/pages/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import {defineConfig} from 'astro/config';
2 | import starlight from '@astrojs/starlight';
3 |
4 | // https://astro.build/config
5 | export default defineConfig({
6 | site: 'https://ibuilding-X.github.io/',
7 | base: '/driver-box',
8 | trailingSlash: "always",
9 | integrations: [
10 | starlight({
11 | title: 'driver-box',
12 | social: {
13 | github: 'https://github.com/ibuilding-X/driver-box',
14 | },
15 | head: [
16 | {
17 | tag: 'script',
18 | content: `
19 | var _hmt = _hmt || [];
20 | (function() {
21 | var hm = document.createElement("script");
22 | hm.src = "https://hm.baidu.com/hm.js?81f653be99c4697c95cedbdacc3023b4";
23 | var s = document.getElementsByTagName("script")[0];
24 | s.parentNode.insertBefore(hm, s);
25 | })();
26 | `
27 | }
28 | ],
29 | sidebar: [
30 | {
31 | label: '使用指南',
32 | autogenerate: {directory: 'guides'},
33 | // items: [
34 | // // Each item here is one entry in the navigation menu.
35 | // { label: '项目简介', link: '/guides/example/' },
36 | // ],
37 | },
38 | {
39 | label: '插件',
40 | autogenerate: {directory: 'plugins'},
41 | },
42 | {
43 | label: 'Export',
44 | autogenerate: {directory: 'export'},
45 | },
46 | {
47 | label: '资产库',
48 | autogenerate: {directory: 'library'},
49 | },
50 | {
51 | label: '开发指南',
52 | autogenerate: {directory: 'developer'},
53 | },
54 | ],
55 | }),
56 | ],
57 | });
58 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/network/read.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
5 | log "github.com/sirupsen/logrus"
6 | )
7 |
8 | type Object struct {
9 | ObjectID btypes.ObjectInstance `json:"object_id"`
10 | ObjectType btypes.ObjectType `json:"object_type"`
11 | Prop btypes.PropertyType `json:"prop"`
12 | ArrayIndex uint32 `json:"array_index"`
13 | }
14 |
15 | func (device *Device) ReadMuti(data btypes.MultiplePropertyData) (out btypes.MultiplePropertyData, err error) {
16 | out, err = device.network.ReadMultiProperty(device.dev, data)
17 | if err != nil {
18 | log.Errorln("network.Read(): err:", err)
19 | return out, err
20 | }
21 | return
22 | }
23 |
24 | func (device *Device) ReadSingle(data btypes.PropertyData) (out btypes.PropertyData, err error) {
25 | out, err = device.network.ReadProperty(device.dev, data)
26 | if err != nil {
27 | log.Errorln("network.Read(): err:", err)
28 | return out, err
29 | }
30 | return out, nil
31 | }
32 |
33 | func (device *Device) Read(obj *Object) (out btypes.PropertyData, err error) {
34 | if obj == nil {
35 | return out, ObjectNil
36 | }
37 | //get object list
38 | rp := btypes.PropertyData{
39 | Object: btypes.Object{
40 | ID: btypes.ObjectID{
41 | Type: obj.ObjectType,
42 | Instance: obj.ObjectID,
43 | },
44 | Properties: []btypes.Property{
45 | {
46 | Type: obj.Prop,
47 | ArrayIndex: obj.ArrayIndex, //bacnet.ArrayAll
48 | },
49 | },
50 | },
51 | }
52 | out, err = device.network.ReadProperty(device.dev, rp)
53 | if err != nil {
54 | if rp.Object.Properties[0].Type == btypes.PropObjectList {
55 | log.Errorln("network.Read(): PropObjectList reads may need to be broken up into multiple reads due to length. Read index 0 for array length err:", err)
56 | } else {
57 | log.Errorln("network.Read(): err:", err)
58 | }
59 | return out, err
60 | }
61 | if len(out.Object.Properties) == 0 {
62 | log.Errorln("network.Read(): no values returned")
63 | return out, nil
64 | }
65 | return out, nil
66 | }
67 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/tsm/transactions_test.go:
--------------------------------------------------------------------------------
1 | package tsm
2 |
3 | import (
4 | "context"
5 | "testing"
6 | "time"
7 | )
8 |
9 | func TestTSM(t *testing.T) {
10 | size := 3
11 | tsm := New(size)
12 | ctx := context.Background()
13 | var err error
14 | for i := 0; i < size-1; i++ {
15 | _, err = tsm.ID(ctx)
16 | if err != nil {
17 | t.Fatal(err)
18 | }
19 | }
20 |
21 | id, err := tsm.ID(ctx)
22 | if err != nil {
23 | t.Fatal(err)
24 | }
25 |
26 | // The buffer should be full at this point.
27 | ctx, cancel := context.WithTimeout(ctx, time.Millisecond)
28 | defer cancel()
29 | _, err = tsm.ID(ctx)
30 | if err == nil {
31 | t.Fatal("Buffer was full but an id was given ")
32 | }
33 |
34 | // Free an ID
35 | err = tsm.Put(id)
36 | if err != nil {
37 | t.Fatal(err)
38 | }
39 |
40 | // Now we should be able to get a new id since we free id
41 | _, err = tsm.ID(context.Background())
42 | if err != nil {
43 | t.Fatal(err)
44 | }
45 |
46 | }
47 |
48 | func TestDataTransaction(t *testing.T) {
49 | size := 2
50 | tsm := New(size)
51 | ids := make([]int, size)
52 | var err error
53 |
54 | for i := 0; i < size; i++ {
55 | ids[i], err = tsm.ID(context.Background())
56 | if err != nil {
57 | t.Fatal(err)
58 | }
59 | }
60 |
61 | go func() {
62 | err = tsm.Send(ids[0], "Hello First ID")
63 | if err != nil {
64 | t.Error(err)
65 | }
66 | }()
67 |
68 | go func() {
69 | err = tsm.Send(ids[1], "Hello Second ID")
70 | if err != nil {
71 | t.Error(err)
72 | }
73 | }()
74 |
75 | go func() {
76 | b, err := tsm.Receive(ids[0], time.Duration(5)*time.Second)
77 | if err != nil {
78 | t.Error(err)
79 | }
80 | s, ok := b.(string)
81 | if !ok {
82 | t.Errorf("type was not preseved")
83 | return
84 | }
85 | t.Log(s)
86 | }()
87 |
88 | b, err := tsm.Receive(ids[1], time.Duration(5)*time.Second)
89 | if err != nil {
90 | t.Error(err)
91 | }
92 |
93 | s, ok := b.(string)
94 | if !ok {
95 | t.Errorf("type was not preseved")
96 | return
97 | }
98 | t.Log(s)
99 | }
100 |
--------------------------------------------------------------------------------
/internal/plugins/websocket/plugin.go:
--------------------------------------------------------------------------------
1 | package websocket
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "github.com/ibuilding-x/driver-box/driverbox/config"
7 | "github.com/ibuilding-x/driver-box/driverbox/helper"
8 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
9 | lua "github.com/yuin/gopher-lua"
10 | "go.uber.org/zap"
11 | "sync"
12 | )
13 |
14 | const ProtocolName = "websocket"
15 |
16 | type Plugin struct {
17 | config config.Config // 核心配置
18 |
19 | connPool map[string]*connector // 连接器
20 | ls *lua.LState // lua 虚拟机
21 | }
22 |
23 | func (p *Plugin) Initialize(logger *zap.Logger, c config.Config, ls *lua.LState) {
24 | p.config = c
25 | p.connPool = make(map[string]*connector)
26 | p.ls = ls
27 |
28 | // 初始化连接池
29 | if err := p.initConnPool(); err != nil {
30 | logger.Error("initialize websocket plugin failed", zap.Error(err))
31 | }
32 |
33 | }
34 |
35 | // Connector 此协议不支持获取连接器
36 | func (p *Plugin) Connector(deviceId string) (connector plugin.Connector, err error) {
37 | // 获取连接key
38 | device, ok := helper.CoreCache.GetDevice(deviceId)
39 | if !ok {
40 | return nil, errors.New("not found device connection key")
41 | }
42 | c, ok := p.connPool[device.ConnectionKey]
43 | if !ok {
44 | return nil, errors.New("not found connection key, key is " + device.ConnectionKey)
45 | }
46 | return c, nil
47 | }
48 |
49 | func (p *Plugin) Destroy() error {
50 | if p.ls != nil {
51 | helper.Close(p.ls)
52 | }
53 | if len(p.connPool) > 0 {
54 | for _, c := range p.connPool {
55 | if c.server != nil {
56 | _ = c.server.Shutdown(context.Background())
57 | }
58 | }
59 | }
60 | return nil
61 | }
62 |
63 | // initConnPool 初始化连接池
64 | func (p *Plugin) initConnPool() (err error) {
65 | for key, _ := range p.config.Connections {
66 | var c connectorConfig
67 | if err = helper.Map2Struct(p.config.Connections[key], &c); err != nil {
68 | return
69 | }
70 | c.ConnectionKey = key
71 | conn := &connector{
72 | config: c,
73 | deviceMappingConn: &sync.Map{},
74 | connMappingDevice: &sync.Map{},
75 | }
76 | conn.startServer()
77 | p.connPool[key] = conn
78 | }
79 | return
80 | }
81 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/writeprop.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
7 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/encoding"
8 | "time"
9 | )
10 |
11 | func (c *client) WriteProperty(device btypes.Device, wp btypes.PropertyData) error {
12 | ctx, cancel := context.WithTimeout(context.Background(), time.Second)
13 | defer cancel()
14 | id, err := c.tsm.ID(ctx)
15 | if err != nil {
16 | return fmt.Errorf("unable to get an transaction id: %v", err)
17 | }
18 | defer c.tsm.Put(id)
19 | device.Addr.SetLength()
20 | npdu := &btypes.NPDU{
21 | Version: btypes.ProtocolVersion,
22 | Destination: &device.Addr,
23 | Source: c.dataLink.GetMyAddress(),
24 | IsNetworkLayerMessage: false,
25 | ExpectingReply: true,
26 | Priority: btypes.Normal,
27 | HopCount: btypes.DefaultHopCount,
28 | }
29 | enc := encoding.NewEncoder()
30 | enc.NPDU(npdu)
31 | enc.WriteProperty(uint8(id), wp)
32 | if enc.Error() != nil {
33 | return enc.Error()
34 | }
35 | // the value filled doesn't matter. it just needs to be non nil
36 | err = fmt.Errorf("go")
37 | for count := 0; err != nil && count < 2; count++ {
38 | var b []byte
39 | var raw interface{}
40 | _, err = c.Send(device.Addr, npdu, enc.Bytes(), nil)
41 | if err != nil {
42 | continue
43 | }
44 | raw, err = c.tsm.Receive(id, time.Duration(5)*time.Second)
45 | if err != nil {
46 | continue
47 | }
48 | switch v := raw.(type) {
49 | case error:
50 | if err == nil {
51 | err = raw.(error)
52 | }
53 | return err
54 | case []byte:
55 | b = v
56 | default:
57 | return fmt.Errorf("received unknown datatype %T", raw)
58 | }
59 |
60 | dec := encoding.NewDecoder(b)
61 | var apdu btypes.APDU
62 | if err = dec.APDU(&apdu); err != nil {
63 | continue
64 | }
65 | if apdu.Error.Class != 0 || apdu.Error.Code != 0 {
66 | err = fmt.Errorf("received error, class: %d, code: %d", apdu.Error.Class, apdu.Error.Code)
67 | continue
68 | }
69 |
70 | return err
71 | }
72 | return err
73 | }
74 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/helpers/data/data.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import (
4 | "fmt"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
6 | )
7 |
8 | func ToBitString(d btypes.PropertyData) (ok bool, out *btypes.BitString) {
9 | out, ok = d.Object.Properties[0].Data.(*btypes.BitString)
10 |
11 | if !ok {
12 | fmt.Println("unable to get object list")
13 | return ok, out
14 | }
15 | return
16 | }
17 |
18 | func ToArr(d btypes.PropertyData) (ok bool, out []interface{}) {
19 | out, ok = d.Object.Properties[0].Data.([]interface{})
20 | if !ok {
21 | fmt.Println("unable to get object list")
22 | return ok, out
23 | }
24 | return
25 | }
26 |
27 | func ToInt(d btypes.PropertyData) (ok bool, out int) {
28 | if len(d.Object.Properties) == 0 {
29 | fmt.Println("No value returned")
30 | return ok, out
31 | }
32 | out, ok = d.Object.Properties[0].Data.(int)
33 | return ok, out
34 | }
35 |
36 | func ToFloat32(d btypes.PropertyData) (ok bool, out float32) {
37 | if len(d.Object.Properties) == 0 {
38 | fmt.Println("No value returned")
39 | return ok, out
40 | }
41 | out, ok = d.Object.Properties[0].Data.(float32)
42 | return ok, out
43 | }
44 |
45 | func ToFloat64(d btypes.PropertyData) (ok bool, out float64) {
46 | if len(d.Object.Properties) == 0 {
47 | fmt.Println("No value returned")
48 | return ok, out
49 | }
50 | out, ok = d.Object.Properties[0].Data.(float64)
51 | return ok, out
52 | }
53 |
54 | func ToBool(d btypes.PropertyData) (ok bool, out bool) {
55 | if len(d.Object.Properties) == 0 {
56 | fmt.Println("No value returned")
57 | return ok, out
58 | }
59 | out, ok = d.Object.Properties[0].Data.(bool)
60 | return ok, out
61 | }
62 |
63 | func ToStr(d btypes.PropertyData) (ok bool, out string) {
64 | if len(d.Object.Properties) == 0 {
65 | fmt.Println("No value returned")
66 | return ok, out
67 | }
68 | out, ok = d.Object.Properties[0].Data.(string)
69 | return ok, out
70 | }
71 |
72 | func ToUint32(d btypes.PropertyData) (ok bool, out uint32) {
73 | if len(d.Object.Properties) == 0 {
74 | fmt.Println("No value returned")
75 | return ok, out
76 | }
77 | out, ok = d.Object.Properties[0].Data.(uint32)
78 | return ok, out
79 | }
80 |
--------------------------------------------------------------------------------
/internal/plugins/httpclient/plugin.go:
--------------------------------------------------------------------------------
1 | package httpclient
2 |
3 | import (
4 | "errors"
5 | "github.com/ibuilding-x/driver-box/driverbox/config"
6 | "github.com/ibuilding-x/driver-box/driverbox/helper"
7 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
8 | lua "github.com/yuin/gopher-lua"
9 | "go.uber.org/zap"
10 | "net/http"
11 | )
12 |
13 | const ProtocolName = "http_client"
14 |
15 | type Plugin struct {
16 | logger *zap.Logger // 日志记录器
17 | config config.Config // 核心配置
18 | connPool map[string]*connector // 连接器
19 | ls *lua.LState // lua 虚拟机
20 | }
21 |
22 | func (p *Plugin) Initialize(logger *zap.Logger, c config.Config, ls *lua.LState) {
23 | p.logger = logger
24 | p.config = c
25 | p.ls = ls
26 |
27 | // 初始化连接池
28 | if err := p.initConnPool(); err != nil {
29 | logger.Error("init connector pool failed", zap.Error(err))
30 | }
31 |
32 | }
33 |
34 | // Connector 此协议不支持获取连接器
35 | func (p *Plugin) Connector(deviceSn string) (connector plugin.Connector, err error) {
36 | // 获取连接key
37 | device, ok := helper.CoreCache.GetDevice(deviceSn)
38 | if !ok {
39 | return nil, errors.New("not found device connection key")
40 | }
41 | c, ok := p.connPool[device.ConnectionKey]
42 | if !ok {
43 | return nil, errors.New("not found connection key, key is " + device.ConnectionKey)
44 | }
45 | return c, nil
46 | }
47 |
48 | func (p *Plugin) Destroy() error {
49 | if p.ls != nil {
50 | helper.Close(p.ls)
51 | }
52 | if len(p.connPool) > 0 {
53 | for i, _ := range p.connPool {
54 | if err := p.connPool[i].Release(); err != nil {
55 | return err
56 | }
57 | }
58 | }
59 | return nil
60 | }
61 |
62 | func (p *Plugin) initConnPool() (err error) {
63 | p.connPool = make(map[string]*connector)
64 | for key, _ := range p.config.Connections {
65 | var c connectorConfig
66 | if err = helper.Map2Struct(p.config.Connections[key], &c); err != nil {
67 | return
68 | }
69 | if c.Timeout <= 0 {
70 | c.Timeout = 5000
71 | }
72 | c.ConnectionKey = key
73 | conn := &connector{
74 | plugin: p,
75 | config: c,
76 | client: &http.Client{},
77 | }
78 | conn.initCollectTask()
79 | p.connPool[key] = conn
80 | }
81 | return
82 | }
83 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/utsm/subscriber.go:
--------------------------------------------------------------------------------
1 | package utsm
2 |
3 | import (
4 | "context"
5 | "sync"
6 | "time"
7 | )
8 |
9 | type subscriber struct {
10 | // Start and End is the range that this object is subscribed to
11 | start int
12 | end int
13 | timeout time.Duration
14 | lastReceivedTimeout time.Duration
15 | lastReceived time.Time
16 | // Data channel is used for data transfer between subscriber and publisher
17 | data chan interface{}
18 | mutex *sync.Mutex
19 | }
20 |
21 | // SubscriberOption are options passed to a particular subscribe function
22 | type SubscriberOption func(s *subscriber)
23 |
24 | // Timeout is the overall timeout for subscribing.
25 | func (s *subscriber) Timeout(d time.Duration) SubscriberOption {
26 | return func(s *subscriber) {
27 | s.mutex.Lock()
28 | defer s.mutex.Unlock()
29 | s.timeout = d
30 | }
31 | }
32 |
33 | // LastReceivedTimeout is a timeout between the last time we have heard from a
34 | // publisher
35 | func (s *subscriber) LastReceivedTimeout(d time.Duration) SubscriberOption {
36 | return func(s *subscriber) {
37 | s.mutex.Lock()
38 | defer s.mutex.Unlock()
39 | s.lastReceivedTimeout = d
40 | }
41 | }
42 |
43 | // getTimeout returns the expiration time based on when we last received a message
44 | func (s *subscriber) getTimeout() time.Duration {
45 | s.mutex.Lock()
46 | // Deadline is x seconds after the last packet we received.
47 | timeout := s.lastReceived.Add(s.lastReceivedTimeout).Sub(time.Now())
48 | s.mutex.Unlock()
49 | return timeout
50 | }
51 |
52 | // Subscribe receives data meant for ids that fall between the start and end range.
53 | func (m *Manager) Subscribe(start int, end int, options ...SubscriberOption) ([]interface{}, error) {
54 | var store []interface{}
55 | s := m.newSubscriber(start, end, options)
56 | defer m.removeSubscriber(s)
57 |
58 | ctx, cancel := context.WithTimeout(context.Background(), s.timeout)
59 | defer cancel()
60 |
61 | for {
62 | c, can := context.WithTimeout(ctx, s.getTimeout())
63 | defer can()
64 |
65 | select {
66 | case <-c.Done():
67 | return store, nil
68 | case b := <-s.data:
69 | store = append(store, b)
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/cmd/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/helpers/homedir"
6 | "os"
7 |
8 | "github.com/spf13/cobra"
9 | "github.com/spf13/viper"
10 | )
11 |
12 | var cfgFile string
13 | var Interface string
14 | var Port int
15 |
16 | // RootCmd represents the base command when called without any subcommands
17 | var RootCmd = &cobra.Command{
18 | Use: "baccli",
19 | Short: "description",
20 | Long: `description`,
21 | }
22 |
23 | // Execute adds all child commands to the root command and sets flags appropriately.
24 | // This is called by main.main(). It only needs to happen once to the rootCmd.
25 | func Execute() {
26 | if err := RootCmd.Execute(); err != nil {
27 | fmt.Println(err)
28 | os.Exit(1)
29 | }
30 | }
31 |
32 | func init() {
33 | cobra.OnInitialize(initConfig)
34 |
35 | RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.baccli.yaml)")
36 | RootCmd.PersistentFlags().StringVarP(&Interface, "interface", "i", "eth0", "Interface e.g. eth0")
37 | RootCmd.PersistentFlags().IntVarP(&Port, "port", "p", int(0xBAC0), "Port")
38 |
39 | RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
40 |
41 | // We want to allow this to be accessed
42 | viper.BindPFlag("interface", RootCmd.PersistentFlags().Lookup("interface"))
43 | viper.BindPFlag("port", RootCmd.PersistentFlags().Lookup("port"))
44 | }
45 |
46 | // initConfig reads in config file and ENV variables if set.
47 | func initConfig() {
48 | if cfgFile != "" {
49 | // Use config file from the flag.
50 | viper.SetConfigFile(cfgFile)
51 | } else {
52 | // Find home directory.
53 | home, err := homedir.Dir()
54 | if err != nil {
55 | fmt.Println(err)
56 | os.Exit(1)
57 | }
58 |
59 | // Search config in home directory with name ".baccli" (without extension).
60 | viper.AddConfigPath(home)
61 | viper.SetConfigName(".baccli")
62 | }
63 |
64 | viper.AutomaticEnv() // read in environment variables that match
65 |
66 | // If a config file is found, read it in.
67 | if err := viper.ReadInConfig(); err == nil {
68 | fmt.Println("Using config file:", viper.ConfigFileUsed())
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/internal/plugins/dlt645/core/serial.go:
--------------------------------------------------------------------------------
1 | package dlt645
2 |
3 | import (
4 | "io"
5 | "sync"
6 | "time"
7 |
8 | "github.com/goburrow/serial"
9 | )
10 |
11 | const (
12 | // SerialDefaultTimeout Serial Default timeout
13 | SerialDefaultTimeout = 1 * time.Second
14 | // SerialDefaultAutoReconnect Serial Default auto reconnect count
15 | SerialDefaultAutoReconnect = 0
16 | )
17 |
18 | // serialPort has configuration and I/O controller.
19 | type serialPort struct {
20 | // Serial port configuration.
21 | serial.Config
22 | mu sync.Mutex
23 | port io.ReadWriteCloser
24 | // if > 0, when disconnect,it will try to reconnect the remote
25 | // but if we active close self,it will not to reconncet
26 | // if == 0 auto reconnect not active
27 | autoReconnect byte
28 | }
29 |
30 | // Connect try to connect the remote server
31 | func (sf *serialPort) Connect() error {
32 | sf.mu.Lock()
33 | err := sf.connect()
34 | sf.mu.Unlock()
35 | return err
36 | }
37 |
38 | // Caller must hold the mutex before calling this method.
39 | func (sf *serialPort) connect() error {
40 | port, err := serial.Open(&sf.Config)
41 | if err != nil {
42 | return err
43 | }
44 | sf.port = port
45 | return nil
46 | }
47 |
48 | // IsConnected returns a bool signifying whether the client is connected or not.
49 | func (sf *serialPort) IsConnected() bool {
50 | sf.mu.Lock()
51 | b := sf.isConnected()
52 | sf.mu.Unlock()
53 | return b
54 | }
55 |
56 | // Caller must hold the mutex before calling this method.
57 | func (sf *serialPort) isConnected() bool {
58 | return sf.port != nil
59 | }
60 |
61 | // SetAutoReconnect set auto reconnect count
62 | // if cnt == 0, disable auto reconnect
63 | // if cnt > 0 ,enable auto reconnect,but max 6
64 | func (sf *serialPort) SetAutoReconnect(cnt byte) {
65 | sf.mu.Lock()
66 | sf.autoReconnect = cnt
67 | if sf.autoReconnect > 6 {
68 | sf.autoReconnect = 6
69 | }
70 | sf.mu.Unlock()
71 | }
72 |
73 | // Close close current connection.
74 | func (sf *serialPort) Close() error {
75 | sf.mu.Lock()
76 | err := sf.close()
77 | sf.mu.Unlock()
78 | return err
79 | }
80 |
81 | func (sf *serialPort) close() error {
82 | var err error
83 | if sf.port != nil {
84 | err = sf.port.Close()
85 | sf.port = nil
86 | }
87 | return err
88 | }
89 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/encoding/date.go:
--------------------------------------------------------------------------------
1 | package encoding
2 |
3 | import "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
4 |
5 | // epochYear is an increment to all non-stored values. This year is chosen in
6 | // the standard. Why? No idea. God help us all if bacnet hits the 255 + 1990
7 | // limit
8 | const epochYear = 1990
9 |
10 | // If the values == 0XFF, that means it is not specified. We will take that to
11 | const notDefined = 0xff
12 |
13 | func IsOddMonth(month int) bool {
14 | return month == 13
15 | }
16 |
17 | func IsEvenMonth(month int) bool {
18 | return month == 14
19 | }
20 |
21 | func IsLastDayOfMonth(day int) bool {
22 | return day == 32
23 | }
24 |
25 | func IsEvenDayOfMonth(day int) bool {
26 | return day == 33
27 | }
28 |
29 | func IsOddDayOfMonth(day int) bool {
30 | return day == 32
31 | }
32 |
33 | func (e *Encoder) date(dt btypes.Date) {
34 | // We don't want to override an unspecified time date
35 | if dt.Year != btypes.UnspecifiedTime {
36 | e.write(uint8(dt.Year - epochYear))
37 | } else {
38 | e.write(uint8(dt.Year))
39 | }
40 | e.write(uint8(dt.Month))
41 | e.write(uint8(dt.Day))
42 | e.write(uint8(dt.DayOfWeek))
43 | }
44 |
45 | func (d *Decoder) date(dt *btypes.Date, length int) {
46 | if length <= 0 {
47 | return
48 | }
49 | data := make([]byte, length)
50 | _, d.err = d.Read(data)
51 | if d.err != nil {
52 | return
53 | }
54 | if len(data) < 4 {
55 | return
56 | }
57 |
58 | if dt.Year != btypes.UnspecifiedTime {
59 | dt.Year = int(data[0]) + epochYear
60 | } else {
61 | dt.Year = int(data[0])
62 | }
63 |
64 | dt.Month = int(data[1])
65 | dt.Day = int(data[2])
66 | dt.DayOfWeek = btypes.DayOfWeek(data[3])
67 | }
68 |
69 | func (e *Encoder) time(t btypes.Time) {
70 | e.write(uint8(t.Hour))
71 | e.write(uint8(t.Minute))
72 | e.write(uint8(t.Second))
73 |
74 | // Stored as 1/100 of a second
75 | e.write(uint8(t.Millisecond / 10))
76 | }
77 | func (d *Decoder) time(t *btypes.Time, length int) {
78 | if length <= 0 {
79 | return
80 | }
81 | data := make([]byte, length)
82 | if _, d.err = d.Read(data); d.err != nil {
83 | return
84 | }
85 |
86 | t.Hour = int(data[0])
87 | t.Minute = int(data[1])
88 | t.Second = int(data[2])
89 | t.Millisecond = int(data[3]) * 10
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/driverbox/export/linkedge/validator.go:
--------------------------------------------------------------------------------
1 | package linkedge
2 |
3 | import "time"
4 |
5 | // intInSlice 检查数字是否在切片中
6 | func intInSlice(num int, nums []int) bool {
7 | for i, _ := range nums {
8 | if num == nums[i] {
9 | return true
10 | }
11 | }
12 | return false
13 | }
14 |
15 | // Verify 验证条件是否满足
16 | func (c YearsCondition) Verify(year int) bool {
17 | return intInSlice(year, c.Years)
18 | }
19 |
20 | // Verify 验证条件是否满足
21 | func (c MonthsCondition) Verify(month int) bool {
22 | return intInSlice(month, c.Months)
23 | }
24 |
25 | // Verify 验证条件是否满足
26 | func (c DaysCondition) Verify(day int) bool {
27 | return intInSlice(day, c.Days)
28 | }
29 |
30 | // Verify 验证条件是否满足
31 | func (c WeeksCondition) Verify(week int) bool {
32 | return intInSlice(week, c.Weeks)
33 | }
34 |
35 | // Verify 验证条件是否满足
36 | func (c TimesCondition) Verify(t time.Time) bool {
37 | beginTime, _ := time.Parse("15:04", c.BeginTime)
38 | endTime, _ := time.Parse("15:04", c.EndTime)
39 |
40 | beginTime = beginTime.AddDate(t.Year(), int(t.Month()), t.Day())
41 | endTime = endTime.AddDate(t.Year(), int(t.Month()), t.Day())
42 |
43 | if t.After(beginTime) && t.Before(endTime) {
44 | return true
45 | }
46 | return false
47 | }
48 |
49 | // Check 检查条件是否合规
50 | func (c YearsCondition) Check() bool {
51 | for i, _ := range c.Years {
52 | if c.Years[i] < 0 || c.Years[i] > 9999 {
53 | return false
54 | }
55 | }
56 | return true
57 | }
58 |
59 | // Check 检查条件是否合规
60 | func (c MonthsCondition) Check() bool {
61 | for i, _ := range c.Months {
62 | if c.Months[i] <= 0 || c.Months[i] > 12 {
63 | return false
64 | }
65 | }
66 | return true
67 | }
68 |
69 | // Check 检查条件是否合规
70 | func (c DaysCondition) Check() bool {
71 | for i, _ := range c.Days {
72 | if c.Days[i] <= 0 || c.Days[i] > 31 {
73 | return false
74 | }
75 | }
76 | return true
77 | }
78 |
79 | // Check 检查条件是否合规
80 | func (c WeeksCondition) Check() bool {
81 | for i, _ := range c.Weeks {
82 | if c.Weeks[i] < 0 || c.Weeks[i] > 6 {
83 | return false
84 | }
85 | }
86 | return true
87 | }
88 |
89 | // Check 检查条件是否合规
90 | func (c TimesCondition) Check() bool {
91 | beginTime, _ := time.Parse("15:04", c.BeginTime)
92 | endTime, _ := time.Parse("15:04", c.EndTime)
93 |
94 | if beginTime.After(endTime) {
95 | return false
96 | }
97 | return true
98 | }
99 |
--------------------------------------------------------------------------------
/internal/plugins/gwplugin/gwplugin.go:
--------------------------------------------------------------------------------
1 | package gwplugin
2 |
3 | import (
4 | "errors"
5 | "github.com/ibuilding-x/driver-box/driverbox/config"
6 | "github.com/ibuilding-x/driver-box/driverbox/helper"
7 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
8 | lua "github.com/yuin/gopher-lua"
9 | "go.uber.org/zap"
10 | )
11 |
12 | // ProtocolName 插件名称
13 | const ProtocolName = "driverbox"
14 |
15 | type gatewayPlugin struct {
16 | l *zap.Logger
17 | c config.Config
18 | ls *lua.LState
19 | connections map[string]*connector
20 | }
21 |
22 | func New() plugin.Plugin {
23 | return &gatewayPlugin{
24 | connections: make(map[string]*connector),
25 | }
26 | }
27 |
28 | func (g *gatewayPlugin) Initialize(logger *zap.Logger, c config.Config, ls *lua.LState) {
29 | g.l = logger
30 | g.c = c
31 | g.ls = ls
32 |
33 | // 初始化连接
34 | if err := g.initConnection(); err != nil {
35 | g.l.Error("init connection failed", zap.Error(err))
36 | }
37 | }
38 |
39 | func (g *gatewayPlugin) Connector(deviceId string) (connector plugin.Connector, err error) {
40 | // 获取连接 key
41 | device, ok := helper.CoreCache.GetDevice(deviceId)
42 | if !ok {
43 | return nil, errors.New("not found device connection key")
44 | }
45 | c, ok := g.connections[device.ConnectionKey]
46 | if !ok {
47 | return nil, errors.New("not found connection key, key is " + device.ConnectionKey)
48 | }
49 | return c, nil
50 | }
51 |
52 | // Destroy 释放所有 ws 连接资源
53 | func (g *gatewayPlugin) Destroy() error {
54 | if len(g.connections) > 0 {
55 | for i, _ := range g.connections {
56 | g.connections[i].destroyed = true
57 | // 关闭 ws 连接
58 | if g.connections[i].conn != nil {
59 | _ = g.connections[i].conn.Close()
60 | }
61 | }
62 | }
63 |
64 | return nil
65 | }
66 |
67 | func (g *gatewayPlugin) initConnection() error {
68 | if len(g.c.Connections) > 0 {
69 | for connKey, _ := range g.c.Connections {
70 | conf := &connectorConfig{}
71 | if err := helper.Map2Struct(g.c.Connections[connKey], conf); err != nil {
72 | return err
73 | }
74 |
75 | // 检查配置项
76 | if err := conf.checkAndRepair(); err != nil {
77 | return err
78 | }
79 |
80 | c := &connector{
81 | conf: *conf,
82 | }
83 |
84 | go c.connect()
85 | g.connections[connKey] = c
86 | }
87 | }
88 |
89 | return nil
90 | }
91 |
--------------------------------------------------------------------------------
/internal/core/common.go:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 |
7 | "github.com/ibuilding-x/driver-box/driverbox/config"
8 | "github.com/ibuilding-x/driver-box/driverbox/helper"
9 | "github.com/ibuilding-x/driver-box/driverbox/helper/utils"
10 | "github.com/ibuilding-x/driver-box/driverbox/library"
11 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
12 | "go.uber.org/zap"
13 | )
14 |
15 | // serialNo 网关序列号
16 | var Metadata = config.Metadata{
17 | SerialNo: "driver-box",
18 | Model: "driver-box",
19 | Vendor: "iBUILDING",
20 | }
21 |
22 | // 校验model有效性
23 | func checkMode(mode plugin.EncodeMode) bool {
24 | switch mode {
25 | case plugin.ReadMode, plugin.WriteMode:
26 | return true
27 | default:
28 | return false
29 | }
30 | }
31 |
32 | // 点位值加工:设备驱动
33 | func deviceDriverProcess(deviceId string, mode plugin.EncodeMode, pointData ...plugin.PointData) ([]plugin.PointData, error) {
34 | device, ok := helper.CoreCache.GetDevice(deviceId)
35 | if !ok {
36 | helper.Logger.Error("unknown device", zap.Any("deviceId", device))
37 | return nil, errors.New("unknown device")
38 | }
39 | scaleEnable := len(device.DriverKey) == 0
40 |
41 | if mode == plugin.WriteMode {
42 | for i, p := range pointData {
43 | point, ok := helper.CoreCache.GetPointByDevice(deviceId, p.PointName)
44 | if !ok {
45 | return nil, fmt.Errorf("not found point, point name is %s", p.PointName)
46 | }
47 | value, err := utils.ConvPointType(p.Value, point.ValueType())
48 | if err != nil {
49 | return nil, err
50 | }
51 | if scaleEnable && point.Scale() != 0 {
52 | value, err = divideStrings(value, point.Scale())
53 | if err != nil {
54 | return nil, err
55 | }
56 | }
57 | pointData[i].Value = value
58 | }
59 | }
60 |
61 | if scaleEnable {
62 | return pointData, nil
63 | }
64 | result := library.Driver().DeviceEncode(device.DriverKey, library.DeviceEncodeRequest{
65 | DeviceId: deviceId,
66 | Mode: mode,
67 | Points: pointData,
68 | })
69 | return result.Points, result.Error
70 | }
71 |
72 | func divideStrings(value interface{}, scale float64) (float64, error) {
73 | switch v := value.(type) {
74 | case float64:
75 | return v / scale, nil
76 | case int64:
77 | return float64(v) / scale, nil
78 | default:
79 | return 0, fmt.Errorf("cannot divide %T with float64", value)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/driverbox/pkg/mbserver/register.go:
--------------------------------------------------------------------------------
1 | package mbserver
2 |
3 | //
4 | //import (
5 | // "errors"
6 | //)
7 | //
8 | //type Register interface {
9 | // Initialize(models []Model, devices []Device) error // 初始化
10 | // SetProperty(did string, property string, value interface{}) error // 设置属性
11 | // GetProperty(did string, property string) (interface{}, error) // 获取属性
12 | // ParseAddress(address uint16) (id string, property string, valueType ValueType, err error) // 解析寄存器地址
13 | // Read(address, quantity uint16) (results []uint16, err error) // 读寄存器
14 | // Write(address, value uint16) error // 写寄存器
15 | //}
16 | //
17 | //type registerImpl struct {
18 | // node *registerNode
19 | //}
20 | //
21 | //func (r *registerImpl) Initialize(models []Model, devices []Device) error {
22 | // node, err := newRegisterNode(models, devices)
23 | // if err != nil {
24 | // return err
25 | // }
26 | // r.node = node
27 | // return nil
28 | //}
29 | //
30 | //func (r *registerImpl) SetProperty(did string, property string, value interface{}) error {
31 | // if r.node == nil {
32 | // return errors.New("register not initialized")
33 | // }
34 | // return r.node.SetProperty(did, property, value)
35 | //}
36 | //
37 | //func (r *registerImpl) GetProperty(did string, property string) (interface{}, error) {
38 | // if r.node == nil {
39 | // return nil, errors.New("register not initialized")
40 | // }
41 | // return r.node.GetProperty(did, property)
42 | //}
43 | //
44 | //func (r *registerImpl) ParseAddress(address uint16) (id string, property string, valueType ValueType, err error) {
45 | // if r.node == nil {
46 | // return "", "", 0, errors.New("register not initialized")
47 | // }
48 | // return r.node.ParseAddress(address)
49 | //}
50 | //
51 | //func (r *registerImpl) Read(address, quantity uint16) (results []uint16, err error) {
52 | // if r.node == nil {
53 | // return nil, errors.New("register not initialized")
54 | // }
55 | // return r.node.Get(address, quantity)
56 | //}
57 | //
58 | //func (r *registerImpl) Write(address, value uint16) error {
59 | // if r.node == nil {
60 | // return errors.New("register not initialized")
61 | // }
62 | // return r.node.Set(address, value)
63 | //}
64 | //
65 | //func NewRegister() Register {
66 | // return ®isterImpl{}
67 | //}
68 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/cmd/cmd/whoIs.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet"
6 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/network"
7 | "time"
8 |
9 | pprint "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/helpers/print"
10 |
11 | "github.com/spf13/cobra"
12 | )
13 |
14 | // Flags
15 | var startRange int
16 | var endRange int
17 |
18 | var outputFilename string
19 |
20 | // whoIsCmd represents the whoIs command
21 | var whoIsCmd = &cobra.Command{
22 | Use: "whois",
23 | Short: "BACnet device discovery",
24 | Long: `whoIs does a bacnet network discovery to find devices in the network
25 | given the provided range.`,
26 | Run: main,
27 | }
28 |
29 | func main(cmd *cobra.Command, args []string) {
30 |
31 | client, err := network.New(&network.Network{Interface: Interface, Port: Port})
32 | if err != nil {
33 | fmt.Println("ERR-client", err)
34 | return
35 | }
36 | defer client.NetworkClose()
37 | go client.NetworkRun()
38 |
39 | if runDiscover {
40 | device, err := network.NewDevice(client, &network.Device{Ip: deviceIP, Port: Port})
41 | if err == nil {
42 | err = device.DeviceDiscover()
43 | }
44 | fmt.Println(err)
45 | return
46 | }
47 |
48 | wi := &bacnet.WhoIsOpts{
49 | High: endRange,
50 | Low: startRange,
51 | GlobalBroadcast: true,
52 | NetworkNumber: uint16(networkNumber),
53 | }
54 | pprint.PrintJOSN(wi)
55 |
56 | fmt.Println("whois 1st")
57 | whoIs, err := client.Whois(wi)
58 | if err != nil {
59 | fmt.Println("ERR-whoIs", err)
60 | return
61 | }
62 | pprint.PrintJOSN(whoIs)
63 |
64 | time.Sleep(time.Second * 3)
65 |
66 | fmt.Println("whois 2nd")
67 | whoIs, err = client.Whois(wi)
68 | if err != nil {
69 | fmt.Println("ERR-whoIs", err)
70 | return
71 | }
72 | pprint.PrintJOSN(whoIs)
73 |
74 | }
75 |
76 | func init() {
77 | RootCmd.AddCommand(whoIsCmd)
78 | whoIsCmd.Flags().BoolVar(&runDiscover, "discover", false, "run network discover")
79 | whoIsCmd.Flags().IntVarP(&startRange, "start", "s", -1, "Start range of discovery")
80 | whoIsCmd.Flags().IntVarP(&endRange, "end", "e", -1, "End range of discovery")
81 | whoIsCmd.Flags().IntVarP(&networkNumber, "network", "", 0, "network number")
82 | whoIsCmd.Flags().StringVarP(&outputFilename, "out", "o", "", "Output results into the given filename in json structure.")
83 | }
84 |
--------------------------------------------------------------------------------
/pages/src/content/docs/guides/about.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: 项目简介
3 | description: A guide in my new Starlight docs site.
4 | sidebar:
5 | order: 1
6 | ---
7 | import { FileTree } from '@astrojs/starlight/components';
8 |
9 | ## 简介
10 | driver-box 是一款支持泛化协议接入的边缘网关框架, 以插件化的形式融合了 Modbus、Bacnet、HTTP、MQTT 等主流协议,同时也支持基于TCP的各类私有化协议对接。
11 |
12 | 
13 |
14 | 我们期望 driver-box 能够为相关人士提供更加高效、舒适的设备接入体验。
15 |
16 | 通过对各类设备的通信协议和数据交互形式进行抽象,定义了一套标准流程以涵盖各类通信协议的共性逻辑,并结合动态解析脚本(Lua)填补其中的差异化部分。
17 |
18 | 以此解决设备接入过程中存在的驱动工程数量爆炸;接入标准难以规范化等问题。
19 |
20 | ### 特性
21 | **免费开源**
22 | 采用商业友好的 Apache-2.0 开源协议,使其成为 IoT 生态圈的极佳选择。
23 |
24 | **架构**
25 | 以 Golang 为主要开发语言,可编译出适配 amd64、arm64、armv7、x86 等系统架构的可执行程序。
26 | 存储空间和运行内存控制在十几MB,满足低规格网关的运行需求。
27 |
28 | 采用高度统一的配置化方式对接各类通讯设备。理想情况下只需编写一个 JSON 文件便可完成设备接入,亦可结合 lua 脚本实现复杂设备的数据加工。
29 |
30 | 通过精心的架构设计,三方用户可无限扩展边缘网关的设备通讯能力和应用服务能力。
31 |
32 | **API**
33 | driver-box 没有提供配套的 UI 界面,但开放了大量实用 RestAPI。用户可以自由设计网关 UI,定制出极致用户体验的边缘产品。
34 |
35 | **应用场景**
36 | driver-box 适用于多种场景,包括智能家居、智慧楼宇、智慧工厂、智慧门店。它促进了设备数据的采集与场景融合,实现了万物皆可连、万物皆可互联、万物皆可智联。
37 |
38 | ## 名词解释
39 | ### 插件(Plugin)
40 | 在 driver-box 中,「**插件**」这个词专指:通讯插件,例如:Http插件、Modbus插件、Bacnet插件。
41 |
42 | 插件是 driver-box 提供了一项开放性能力,如若内置的插件不满足需求,用户可参考《[通讯插件开发](/driver-box/developer/plugin/)》实现一款自定义插件并集成至 driver-box。
43 |
44 | ### Export
45 | Export,一时找不到合适的中文名词来表示这个单词在 driver-box 中的用途。
46 |
47 | 它也是 driver-box 提供的一项开放性能力,用于处理 driver-box 向上层传递的设备数据和事件。
48 |
49 | 以此,我们可以实现类似场景联动、边缘计算、数据上云等一系列高级能力。而这些能力的组织与融合,便形成了完整的边缘引擎产品。
50 |
51 | [《Export 开发》](/driver-box/developer/export/)。
52 |
53 | ### 资产库
54 | 资产库是 driver-box 框架提供的一种资源管理能力。通过持续沉淀和复用资产库中的已有资源,逐渐提升项目工程实施效率。
55 |
56 | :::tip[为什么叫资产库,而不是资源库?]
57 | 虽然本质上都是一些配置型资源文件,但我们认为这些资源文件都是企业实际项目的宝贵经验积累。
58 | 这份积累能够在帮助企业极大的提升产品后期推广、复制效率,从某种角度而言,它更像企业的一份独特资产。
59 |
60 | :::
61 |
62 | 资产库存储于`res/library/`目录。
63 |
64 |
65 | - driver-box
66 | - res
67 | - library
68 | - index.json 资产库索引文件,便于快速定位资源
69 | - driver/ 设备层驱动库
70 | - mirror_tpl/ 镜像模块库
71 | - model/ 物模型库
72 | - protocol/ 通信协议层驱动库
73 |
74 |
75 | ### 虚拟设备
76 | 虚拟设备是 driver-box 框架提供的一种设备通讯方式模拟能力。
77 | 使用户可在无需对接真实设备的情况下,进行本地设备配置和调试。
78 |
79 | 开启虚拟设备模式,只需将 connections 中的 `virtual` 配置项设置为 `true`。
80 |
81 | 现以支持以下几种通讯插件:
82 | - Bacnet
83 | - [Modbus](../../plugins/modbus/#虚拟设备)
84 |
85 | ### Event
86 | Event 是 driver-box 框架提供的一种事件通知能力。
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/readprop.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
7 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/encoding"
8 | "log"
9 | "time"
10 | )
11 |
12 | // ReadProperty reads a single property from a single object in the given device.
13 | func (c *client) ReadProperty(device btypes.Device, rp btypes.PropertyData) (btypes.PropertyData, error) {
14 | ctx, cancel := context.WithTimeout(context.Background(), time.Second)
15 | defer cancel()
16 | id, err := c.tsm.ID(ctx)
17 | if err != nil {
18 | return btypes.PropertyData{}, fmt.Errorf("unable to get an transaction id: %v", err)
19 | }
20 | defer c.tsm.Put(id)
21 | enc := encoding.NewEncoder()
22 | device.Addr.SetLength()
23 | npdu := &btypes.NPDU{
24 | Version: btypes.ProtocolVersion,
25 | Destination: &device.Addr,
26 | Source: c.dataLink.GetMyAddress(),
27 | IsNetworkLayerMessage: false,
28 | ExpectingReply: true,
29 | Priority: btypes.Normal,
30 | HopCount: btypes.DefaultHopCount,
31 | }
32 | enc.NPDU(npdu)
33 |
34 | err = enc.ReadProperty(uint8(id), rp)
35 | if enc.Error() != nil || err != nil {
36 | return btypes.PropertyData{}, err
37 | }
38 |
39 | // the value filled doesn't matter. it just needs to be non nil
40 | err = fmt.Errorf("go")
41 | for count := 0; err != nil && count < retryCount; count++ {
42 | var b []byte
43 | var out btypes.PropertyData
44 | _, err = c.Send(device.Addr, npdu, enc.Bytes(), nil)
45 | if err != nil {
46 | log.Print(err)
47 | continue
48 | }
49 |
50 | var raw interface{}
51 | raw, err = c.tsm.Receive(id, time.Duration(5)*time.Second)
52 | if err != nil {
53 | continue
54 | }
55 | switch v := raw.(type) {
56 | case error:
57 | return out, v
58 | case []byte:
59 | b = v
60 | default:
61 | return out, fmt.Errorf("received unknown datatype %T", raw)
62 | }
63 |
64 | dec := encoding.NewDecoder(b)
65 |
66 | var apdu btypes.APDU
67 | if err = dec.APDU(&apdu); err != nil {
68 | continue
69 | }
70 | if apdu.Error.Class != 0 || apdu.Error.Code != 0 {
71 | err = fmt.Errorf("received error, class: %d, code: %d", apdu.Error.Class, apdu.Error.Code)
72 | continue
73 | }
74 |
75 | if err = dec.ReadProperty(&out); err != nil {
76 | continue
77 | }
78 | return out, dec.Error()
79 | }
80 | return btypes.PropertyData{}, err
81 | }
82 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/helpers/nils/nil.go:
--------------------------------------------------------------------------------
1 | package nils
2 |
3 | func NewString(value string) *string {
4 | return &value
5 | }
6 |
7 | func StringIsNil(b *string) string {
8 | if b == nil {
9 | return ""
10 | } else {
11 | return *b
12 | }
13 | }
14 | func StringNilCheck(b *string) bool {
15 | if b == nil {
16 | return true
17 | } else {
18 | return false
19 | }
20 | }
21 |
22 | func Float64IsNil(b *float64) float64 {
23 | if b == nil {
24 | return 0
25 | } else {
26 | return *b
27 | }
28 | }
29 |
30 | func NewInt(value int) *int {
31 | return &value
32 | }
33 |
34 | func NewBool(value bool) *bool {
35 | return &value
36 | }
37 |
38 | func BoolIsNil(value *bool) bool {
39 | if value == nil {
40 | return false
41 | }
42 | return *value
43 | }
44 |
45 | func NewTrue() *bool {
46 | b := true
47 | return &b
48 | }
49 |
50 | func NewFalse() *bool {
51 | b := false
52 | return &b
53 | }
54 |
55 | func NewUint16(value uint16) *uint16 {
56 | return &value
57 | }
58 |
59 | func NewUint32(value uint32) *uint32 {
60 | return &value
61 | }
62 |
63 | func NewFloat32(value float32) *float32 {
64 | return &value
65 | }
66 |
67 | func NewFloat64(value float64) *float64 {
68 | return &value
69 | }
70 |
71 | func IntIsNil(b *int) int {
72 | if b == nil {
73 | return 0
74 | } else {
75 | return *b
76 | }
77 | }
78 |
79 | func BoolNilCheck(b *bool) bool {
80 | if b == nil {
81 | return true
82 | } else {
83 | return false
84 | }
85 | }
86 |
87 | func IntNilCheck(b *int) bool {
88 | if b == nil {
89 | return true
90 | } else {
91 | return false
92 | }
93 | }
94 |
95 | func Float32IsNil(b *float32) float32 {
96 | if b == nil {
97 | return 0
98 | } else {
99 | return *b
100 | }
101 | }
102 |
103 | func UnitIsNil(b *uint) uint {
104 | if b == nil {
105 | return 0
106 | } else {
107 | return *b
108 | }
109 | }
110 |
111 | func Unit16IsNil(b *uint16) uint16 {
112 | if b == nil {
113 | return 0
114 | } else {
115 | return *b
116 | }
117 | }
118 |
119 | func Unit32IsNil(b *uint32) uint32 {
120 | if b == nil {
121 | return 0
122 | } else {
123 | return *b
124 | }
125 | }
126 |
127 | func Unit32NilCheck(b *uint32) bool {
128 | if b == nil {
129 | return true
130 | } else {
131 | return false
132 | }
133 | }
134 |
135 | func FloatIsNilCheck(b *float64) bool {
136 | if b == nil {
137 | return true
138 | } else {
139 | return false
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/internal/export/ai/mcp/tools/history.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/ibuilding-x/driver-box/driverbox/export/history"
7 | "github.com/ibuilding-x/driver-box/driverbox/helper"
8 | "github.com/mark3labs/mcp-go/mcp"
9 | "go.uber.org/zap"
10 | )
11 |
12 | var HistoryTableSchemaTool = mcp.NewTool("history_table_schema",
13 | mcp.WithDescription("查询当前网关数据库的表结构定义,有助于大模型编写正确的SQL语句开展数据分析"),
14 | )
15 |
16 | var HistoryTableSchemaHandler = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
17 | r, e := history.NewExport().QueryDataBySql("SELECT name,sql FROM sqlite_master WHERE type='table';")
18 | helper.Logger.Info("查询当前网关数据库的表结构定义", zap.Any("result", r), zap.Error(e))
19 | if e != nil {
20 | return nil, e
21 | }
22 | markdown := fmt.Sprintf("## 表结构定义(共 %d 张表)\n\n", len(r))
23 | for _, v := range r {
24 | markdown += fmt.Sprintf("### tableName: %s\n\n```sql\n%s\n```\n\n", v["name"], v["sql"])
25 | }
26 | return mcp.NewToolResultText(markdown), nil
27 | }
28 |
29 | var HistoryDataAnalysisTool = mcp.NewTool("history_data_analysis",
30 | mcp.WithDescription("执行大模型生成的SQL查询语句。要求:查询SQL必须是网关中存在的表和字段,且符合 sqlite 语法;涉及设备相关数据查询前,确保已通过其他Tool [ `"+CoreCacheGetModelByNameTool.Name+"` 或 `"+CoreCacheGetModelByDeviceTool.Name+"` ]明确知晓设备和物模型的相关字段名定义;优先使用统计类函数,避免出现大量数据扫描。"),
31 | mcp.WithString("sql", mcp.Required(), mcp.Description("要执行的SQL查询语句")),
32 | )
33 |
34 | var HistoryDataAnalysisHandler = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
35 | sql := request.GetString("sql", "")
36 | if sql == "" {
37 | return nil, fmt.Errorf("请提供有效的SQL查询语句")
38 | }
39 | helper.Logger.Info("执行大模型生成的SQL查询语句", zap.String("sql", sql))
40 | r, e := history.NewExport().QueryDataBySql(sql)
41 |
42 | if e != nil {
43 | return nil, e
44 | }
45 | if len(r) == 0 {
46 | return mcp.NewToolResultText("无结果"), nil
47 | }
48 | markdown := fmt.Sprintf("## 查询结果(共 %d 条记录)\n\n", len(r))
49 | markdown += "|"
50 | for k, _ := range r[0] {
51 | markdown += fmt.Sprintf("%s |", k)
52 | }
53 | markdown += "\n|"
54 | for _, _ = range r[0] {
55 | markdown += fmt.Sprintf("---|")
56 | }
57 | markdown += "\n"
58 | for _, v := range r {
59 | markdown += "|"
60 | for k, _ := range v {
61 | if v[k] == nil {
62 | markdown += " |"
63 | } else {
64 | markdown += fmt.Sprintf("%s |", v[k])
65 | }
66 | }
67 | markdown += "\n"
68 | }
69 | helper.Logger.Info("执行大模型生成的SQL查询语句", zap.Any("result", r), zap.Error(e))
70 | return mcp.NewToolResultText(markdown), nil
71 | }
72 |
--------------------------------------------------------------------------------
/driverbox/pkg/mbserver/common.go:
--------------------------------------------------------------------------------
1 | package mbserver
2 |
3 | const (
4 | ValueTypeBool = iota
5 | ValueTypeUint8
6 | ValueTypeInt8
7 | ValueTypeUint16
8 | ValueTypeInt16
9 | ValueTypeUint32
10 | ValueTypeInt32
11 | ValueTypeUint64
12 | ValueTypeInt64
13 | ValueTypeUint
14 | ValueTypeInt
15 | ValueTypeFloat32
16 | ValueTypeFloat64
17 | )
18 |
19 | const (
20 | AccessRead = iota
21 | AccessWrite
22 | AccessReadWrite
23 | )
24 |
25 | type Model struct {
26 | Id string `json:"id"`
27 | Name string `json:"name"` // 可选
28 | Properties []Property `json:"properties"`
29 | }
30 |
31 | type Property struct {
32 | Name string `json:"name"`
33 | Description string `json:"description"` // 可选
34 | ValueType int `json:"valueType"`
35 | Access int `json:"access"`
36 | }
37 |
38 | type Device struct {
39 | ModelId string `json:"modelId"`
40 | Id string `json:"id"`
41 | }
42 |
43 | type DeviceUnit struct {
44 | Id string `json:"id"`
45 | ModelId string `json:"modelId"`
46 | ModelName string `json:"modelName"`
47 | Properties []PropertyUnit `json:"properties"`
48 | }
49 |
50 | type PropertyUnit struct {
51 | Name string `json:"name"`
52 | Description string `json:"description"`
53 | Type string `json:"type"`
54 | Access string `json:"access"`
55 | StartAddress uint16 `json:"address"`
56 | Quantity uint16 `json:"quantity"`
57 | HumanAddress string `json:"humanAddress"`
58 | }
59 |
60 | func ValueTypeText(valueType int) string {
61 | switch valueType {
62 | case ValueTypeBool:
63 | return "bool"
64 | case ValueTypeUint8:
65 | return "uint8"
66 | case ValueTypeInt8:
67 | return "int8"
68 | case ValueTypeUint16:
69 | return "uint16"
70 | case ValueTypeInt16:
71 | return "int16"
72 | case ValueTypeUint32:
73 | return "uint32"
74 | case ValueTypeInt32:
75 | return "int32"
76 | case ValueTypeUint64:
77 | return "uint64"
78 | case ValueTypeInt64:
79 | return "int64"
80 | case ValueTypeUint:
81 | return "uint"
82 | case ValueTypeInt:
83 | return "int"
84 | case ValueTypeFloat32:
85 | return "float32"
86 | case ValueTypeFloat64:
87 | return "float64"
88 | default:
89 | return ""
90 | }
91 | }
92 |
93 | func AccessText(access int) string {
94 | switch access {
95 | case AccessRead:
96 | return "R"
97 | case AccessWrite:
98 | return "W"
99 | case AccessReadWrite:
100 | return "RW"
101 | default:
102 | return ""
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/driverbox/export/mqtt_export.go:
--------------------------------------------------------------------------------
1 | package export
2 |
3 | import (
4 | "crypto/tls"
5 | "encoding/json"
6 | mqtt "github.com/eclipse/paho.mqtt.golang"
7 | "github.com/ibuilding-x/driver-box/driverbox/event"
8 | "github.com/ibuilding-x/driver-box/driverbox/plugin"
9 | "go.uber.org/zap"
10 | "log"
11 | "time"
12 | )
13 |
14 | type MqttExport struct {
15 | Broker string `json:"broker"`
16 | Username string `json:"username"`
17 | Password string `json:"password"`
18 | ClientID string `json:"client_id"`
19 | init bool
20 | client mqtt.Client
21 | handler mqtt.MessageHandler
22 | ExportTopic string
23 | }
24 |
25 | func (export *MqttExport) Init() error {
26 | if len(export.ExportTopic) == 0 {
27 | panic("exportTopic is blank")
28 | }
29 | options := mqtt.NewClientOptions()
30 | options.AddBroker(export.Broker)
31 | options.SetUsername(export.Username)
32 | options.SetPassword(export.Password)
33 | options.SetClientID(export.ClientID)
34 | // tsl 设置
35 | if options.Servers[0].Scheme == "ssl" {
36 | options.SetTLSConfig(&tls.Config{
37 | InsecureSkipVerify: true,
38 | })
39 | }
40 | options.SetOnConnectHandler(export.onConnectHandler)
41 | options.SetConnectionLostHandler(export.onConnectionLostHandler)
42 | export.client = mqtt.NewClient(options)
43 | token := export.client.Connect()
44 | if token.WaitTimeout(5*time.Second) && token.Error() != nil {
45 | return token.Error()
46 | }
47 | return nil
48 | }
49 |
50 | // onConnectHandler 连接成功
51 | func (export *MqttExport) onConnectHandler(client mqtt.Client) {
52 | log.Println("mqttExport init success")
53 | export.init = true
54 | }
55 |
56 | // onConnectionLostHandler 连接丢失
57 | func (export *MqttExport) onConnectionLostHandler(client mqtt.Client, err error) {
58 | log.Fatal("local mqtt connect lost", zap.Error(err))
59 | }
60 |
61 | // ExportTo 导出消息:写入Edgex总线、MQTT上云
62 | func (export *MqttExport) ExportTo(deviceData plugin.DeviceData) {
63 | log.Println("export...")
64 | bytes, _ := json.Marshal(deviceData)
65 | token := export.client.Publish(export.ExportTopic, 0, false, bytes)
66 | if token.Error() != nil {
67 | log.Fatal(token.Error())
68 | }
69 | }
70 |
71 | // 继承Export OnEvent接口
72 | func (export *MqttExport) OnEvent(eventCode string, key string, eventValue interface{}) error {
73 | if event.EventCodeDeviceStatus == eventCode {
74 | export.client.Publish("/driverbox/event/"+export.ClientID, 0, false, map[string]any{"deviceId": key, "online": eventValue})
75 | }
76 | return nil
77 | }
78 |
79 | func (export *MqttExport) IsReady() bool {
80 | return export.init
81 | }
82 |
--------------------------------------------------------------------------------
/pages/README.md:
--------------------------------------------------------------------------------
1 | # Starlight Starter Kit: Basics
2 |
3 | [](https://starlight.astro.build)
4 |
5 | ```
6 | npm create astro@latest -- --template starlight
7 | ```
8 |
9 | [](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
10 | [](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
11 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
12 |
13 | > 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
14 |
15 | ## 🚀 Project Structure
16 |
17 | Inside of your Astro + Starlight project, you'll see the following folders and files:
18 |
19 | ```
20 | .
21 | ├── public/
22 | ├── src/
23 | │ ├── assets/
24 | │ ├── content/
25 | │ │ ├── docs/
26 | │ │ └── config.ts
27 | │ └── env.d.ts
28 | ├── astro.config.mjs
29 | ├── package.json
30 | └── tsconfig.json
31 | ```
32 |
33 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
34 |
35 | Images can be added to `src/assets/` and embedded in Markdown with a relative link.
36 |
37 | Static assets, like favicons, can be placed in the `public/` directory.
38 |
39 | ## 🧞 Commands
40 |
41 | All commands are run from the root of the project, from a terminal:
42 |
43 | | Command | Action |
44 | | :------------------------ | :----------------------------------------------- |
45 | | `npm install` | Installs dependencies |
46 | | `npm run dev` | Starts local dev server at `localhost:4321` |
47 | | `npm run build` | Build your production site to `./dist/` |
48 | | `npm run preview` | Preview your build locally, before deploying |
49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
50 | | `npm run astro -- --help` | Get help using the Astro CLI |
51 |
52 | ## 👀 Want to learn more?
53 |
54 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
55 |
--------------------------------------------------------------------------------
/internal/plugins/bacnet/bacnet/writemulti.go:
--------------------------------------------------------------------------------
1 | package bacnet
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/btypes"
7 | "github.com/ibuilding-x/driver-box/internal/plugins/bacnet/bacnet/encoding"
8 | "time"
9 | )
10 |
11 | func (c *client) WriteMultiProperty(dev btypes.Device, wp btypes.MultiplePropertyData) error {
12 | ctx, cancel := context.WithTimeout(context.Background(), time.Second)
13 | defer cancel()
14 | id, err := c.tsm.ID(ctx)
15 | if err != nil {
16 | return fmt.Errorf("unable to get an transaction id: %v", err)
17 | }
18 | defer c.tsm.Put(id)
19 |
20 | npdu := &btypes.NPDU{
21 | Version: btypes.ProtocolVersion,
22 | Destination: &dev.Addr,
23 | Source: c.dataLink.GetMyAddress(),
24 | IsNetworkLayerMessage: false,
25 | ExpectingReply: true,
26 | Priority: btypes.Normal,
27 | HopCount: btypes.DefaultHopCount,
28 | }
29 | enc := encoding.NewEncoder()
30 | enc.NPDU(npdu)
31 |
32 | enc.WriteMultiProperty(uint8(id), wp)
33 | if enc.Error() != nil {
34 | return enc.Error()
35 | }
36 |
37 | pack := enc.Bytes()
38 | if dev.MaxApdu < uint32(len(pack)) {
39 | return fmt.Errorf("read multiple property is too large (max: %d given: %d)", dev.MaxApdu, len(pack))
40 | }
41 |
42 | // the value filled doesn't matter. it just needs to be non nil
43 | err = fmt.Errorf("go")
44 |
45 | for count := 0; err != nil && count < maxReattempt; count++ {
46 | err = c.sendWriteMultipleProperty(id, dev, npdu, pack)
47 | if err == nil {
48 | return nil
49 | }
50 | }
51 | return fmt.Errorf("failed %d tries: %v", maxReattempt, err)
52 | }
53 |
54 | func (c *client) sendWriteMultipleProperty(id int, dev btypes.Device, npdu *btypes.NPDU, request []byte) error {
55 | _, err := c.Send(dev.Addr, npdu, request, nil)
56 | if err != nil {
57 | return err
58 | }
59 |
60 | raw, err := c.tsm.Receive(id, time.Duration(5)*time.Second)
61 | if err != nil {
62 | return fmt.Errorf("unable to receive id %d: %v", id, err)
63 | }
64 |
65 | var b []byte
66 | switch v := raw.(type) {
67 | case error:
68 | return v
69 | case []byte:
70 | b = v
71 | default:
72 | return fmt.Errorf("received unknown datatype %T", raw)
73 | }
74 |
75 | dec := encoding.NewDecoder(b)
76 |
77 | var apdu btypes.APDU
78 | if err = dec.APDU(&apdu); err != nil {
79 | return err
80 | }
81 | if apdu.Error.Class != 0 || apdu.Error.Code != 0 {
82 | return fmt.Errorf("received error, class: %d, code: %d", apdu.Error.Class, apdu.Error.Code)
83 | }
84 | return nil
85 | }
86 |
--------------------------------------------------------------------------------
/internal/logger/logger.go:
--------------------------------------------------------------------------------
1 | package logger
2 |
3 | import (
4 | "go.uber.org/zap"
5 | "go.uber.org/zap/zapcore"
6 | "gopkg.in/natefinch/lumberjack.v2"
7 | "io"
8 | "os"
9 | "strings"
10 | "time"
11 | )
12 |
13 | // Logger 日志记录器
14 | var Logger *zap.Logger
15 |
16 | // customClock 自定义时钟(时区调整)
17 | type customClock struct {
18 | }
19 |
20 | func (c *customClock) Now() time.Time {
21 | timezone := time.FixedZone("Asia/Shanghai", 8*3600)
22 | return time.Now().In(timezone)
23 | }
24 |
25 | func (c *customClock) NewTicker(duration time.Duration) *time.Ticker {
26 | return time.NewTicker(duration)
27 | }
28 |
29 | // New 实例化
30 | func InitLogger(logPath, level string) {
31 | config := zap.NewProductionConfig()
32 | config.Level = convertLevel(level) // 设置日志级别
33 | config.Encoding = "console" // 输出格式:console、json
34 | config.EncoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000") // 输出时间格式
35 | config.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder // 输出等级格式
36 | config.EncoderConfig.ConsoleSeparator = " | " // 字段分割符
37 |
38 | var options []zap.Option
39 | options = append(options, zap.AddCaller()) // 输出调用者信息
40 | options = append(options, zap.WithClock(&customClock{})) // 设置时区
41 | options = append(options, zap.AddStacktrace(zap.ErrorLevel)) // 错误堆栈信息
42 | options = append(options, zap.Fields(zap.Int("pid", os.Getpid()))) // 进程ID
43 |
44 | var w io.Writer
45 | if logPath == "" {
46 | w = os.Stdout
47 | } else {
48 | w = &lumberjack.Logger{
49 | Filename: logPath,
50 | MaxSize: 100,
51 | MaxAge: 15,
52 | MaxBackups: 10,
53 | LocalTime: true,
54 | Compress: true,
55 | }
56 | }
57 |
58 | encoder := zapcore.NewConsoleEncoder(config.EncoderConfig)
59 | writer := zapcore.NewMultiWriteSyncer(zapcore.AddSync(w), zapcore.AddSync(ChanWriter))
60 | core := zapcore.NewCore(encoder, writer, config.Level)
61 |
62 | Logger = zap.New(core, options...)
63 | }
64 |
65 | // convertLevel 等级转换
66 | func convertLevel(level string) zap.AtomicLevel {
67 | switch strings.ToLower(level) {
68 | case "debug":
69 | return zap.NewAtomicLevelAt(zap.DebugLevel)
70 | case "info":
71 | return zap.NewAtomicLevelAt(zap.InfoLevel)
72 | case "warn":
73 | return zap.NewAtomicLevelAt(zap.WarnLevel)
74 | case "error":
75 | return zap.NewAtomicLevelAt(zap.ErrorLevel)
76 | default:
77 | return zap.NewAtomicLevelAt(zap.DebugLevel)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/internal/export/ai/coordinator_agent.go:
--------------------------------------------------------------------------------
1 | package ai
2 |
3 | import (
4 | "context"
5 | "github.com/ibuilding-x/driver-box/driverbox/helper"
6 | agent2 "github.com/ibuilding-x/driver-box/internal/export/ai/agent"
7 | "github.com/tmc/langchaingo/agents"
8 | "github.com/tmc/langchaingo/chains"
9 | "github.com/tmc/langchaingo/tools"
10 | "go.uber.org/zap"
11 | )
12 |
13 | func (export *Export) startAgent() error {
14 | mcpTools, e := export.getLangChainTools()
15 |
16 | if e != nil {
17 | return e
18 | }
19 |
20 | ctx := context.Background()
21 |
22 | dataAnalysisAgent := &agent2.DataAnalysisAgent{
23 | LLM: export.llm,
24 | Tools: mcpTools,
25 | }
26 | deviceManagerAgent := &agent2.DeviceManagerAgent{
27 | LLM: export.llm,
28 | Tools: mcpTools,
29 | }
30 |
31 | tool := make([]tools.Tool, 0)
32 | //tool = append(tool, mcpTools...)
33 | tool = append(tool, dataAnalysisAgent)
34 | tool = append(tool, deviceManagerAgent)
35 |
36 | agent := agents.NewOneShotAgent(
37 | export.llm,
38 | tool,
39 | agents.WithMaxIterations(3),
40 | agents.WithPromptPrefix(`今天是 {{.today}}.
41 | 您是边缘网关协调代理(Coordinator Agent),负责统筹多个专业子代理及工具以完成用户请求。
42 |
43 | 您的核心职责:
44 | 1. 分析用户输入,理解需求并制定执行计划。
45 | 2. 协调各专业代理(如数据分析代理、设备管理代理等)之间的协作流程。
46 | 3. 在每一步骤中为下游代理提供**完整上下文与明确意图**。
47 | 4. 确保每次只调用一个 agent 或 tool,并在响应返回后再继续下一步。
48 | 5. 遇到失败或异常情况时尝试替代方案或提供清晰的问题反馈。
49 | 6. 在必要时引导其他 agent 向您寻求帮助,并协助其完成复杂任务。
50 |
51 | 协作规则:
52 | - 始终从制定清晰的执行计划开始。
53 | - 每次调用 agent/tool 时,必须提供当前上下文摘要、可用资源列表以及调用目的。
54 | - 所有中间结果需共享给后续步骤使用。
55 | - 如果发现执行结果不理想,应考虑调整提示词并重新运行相关 agent。
56 | - 对于关键性任务,要求被调用 agent 提供详细过程日志以便追溯。
57 |
58 | Available tools:
59 | {{.tool_descriptions}}`),
60 | agents.WithPromptSuffix(`Begin!
61 |
62 | Question: {{.input}}
63 | {{.agent_scratchpad}}`),
64 | agents.WithPromptFormatInstructions(`Use the following format:
65 |
66 | Thought: [Explain your reasoning]
67 | Plan: [Outline the execution plan if not already defined]
68 | Action: [Choose a tool/agent from [{{.tool_names}}]]
69 | Action Input: {"input": "user question or context"}
70 | Observation: [Result returned by the tool/agent]
71 | ... (repeat as needed)
72 | Final Answer: [Summarize findings or instructions]
73 |
74 | 注意:
75 | - 每个 action 必须是原子且定义明确的操作。
76 | - 调用任何 agent 时都应在 Action Input 中包含足够的上下文信息,这些信息以合适的结构包含在 input 字段中。
77 | - 若遇到不确定的情况,请优先调用具备查询能力的 agent 获取更多信息`),
78 | agents.WithOutputKey("output"),
79 | )
80 | executor := agents.NewExecutor(agent)
81 | // Use the agent
82 | question := "按照设备类型进行归类统计"
83 | result, _ := chains.Run(
84 | ctx,
85 | executor,
86 | question,
87 | )
88 | helper.Logger.Info("执行完毕", zap.Any("result", result))
89 | return nil
90 | }
91 |
--------------------------------------------------------------------------------
/res/ui/devices.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Device List
7 |
8 |
9 |
33 |
34 |
35 |
36 |
49 |
Device List
50 |
51 |
52 |
53 | | Name |
54 | ID |
55 | Status |
56 | ModelID |
57 | 插件类型 |
58 | 连接标识 |
59 | Action |
60 |
61 |
62 |
63 | {{range .Devices}}
64 |
65 | | {{.Name}} |
66 | {{.ID}} |
67 | {{.Status}} |
68 | {{.ModeId}} |
69 | {{.Plugin}} |
70 | {{.Connection}} |
71 | View Details |
72 |
73 | {{end}}
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------