├── 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 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/ibuilding-X/driver-box) 3 | 4 | ## **我正在参加 Gitee 2025 最受欢迎的开源软件投票活动,快来给我投票吧![https://gitee.com/activity/2025opensource?ident=ITVLU3](https://gitee.com/activity/2025opensource?ident=ITVLU3)** 5 | 投过票的有缘人进交流群前可向群主报备一下,后续提供专属的技术咨询服务。 6 | 7 | 8 | ## 文档 9 | 10 | [快速开始](https://ibuilding-x.github.io/driver-box/) 11 | 12 | ## 简介 13 | driver-box 是一款支持泛化协议接入的边缘网关框架, 以插件化的形式融合了 Modbus、Bacnet、HTTP、MQTT 等主流协议,同时也支持基于TCP的各类私有化协议对接。 14 | ![](https://ibuilding-x.github.io/driver-box/framework.svg) 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 | ![](/driver-box/framework.svg) 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 | [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) 4 | 5 | ``` 6 | npm create astro@latest -- --template starlight 7 | ``` 8 | 9 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) 10 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) 11 | [![Deploy with Vercel](https://vercel.com/button)](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 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | {{range .Devices}} 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {{end}} 74 | 75 |
NameIDStatusModelID插件类型连接标识Action
{{.Name}}{{.ID}}{{.Status}}{{.ModeId}}{{.Plugin}}{{.Connection}}View Details
76 |
77 | 78 | 79 | --------------------------------------------------------------------------------