From 85bd8de2a2acd8530f84050d0fbe2be54ce72a82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrian=20Z=C3=BCrcher?= Date: Fri, 25 Apr 2025 18:56:27 +0200 Subject: [PATCH] add files --- datapoints.go | 369 ++++++++++++++++++++++++++++++++++++++++++++++++ drivers.go | 20 +++ get.go | 13 ++ jsonData.go | 37 +++++ jsonResponse.go | 13 ++ port.go | 6 + publish.go | 11 ++ query.go | 6 + rights.go | 18 +++ set.go | 12 ++ subscribe.go | 38 +++++ subscribed.go | 9 ++ subscribtion.go | 13 ++ type.go | 102 +++++++++++++ 14 files changed, 667 insertions(+) create mode 100644 datapoints.go create mode 100644 drivers.go create mode 100644 get.go create mode 100644 jsonData.go create mode 100644 jsonResponse.go create mode 100644 port.go create mode 100644 publish.go create mode 100644 query.go create mode 100644 rights.go create mode 100644 set.go create mode 100644 subscribe.go create mode 100644 subscribed.go create mode 100644 subscribtion.go create mode 100644 type.go diff --git a/datapoints.go b/datapoints.go new file mode 100644 index 0000000..7974fd5 --- /dev/null +++ b/datapoints.go @@ -0,0 +1,369 @@ +package models + +import ( + "errors" + "fmt" + "regexp" + "strings" + "time" + + "github.com/coder/websocket/wsjson" + "github.com/google/uuid" + serverModels "github.com/tecamino/tecamino-dbm/server/models" + "github.com/tecamino/tecamino-dbm/utils" +) + +const ( + OnCreate = "onCreate" + OnChange = "onChange" + OnDelete = "onDelete" +) + +type Datapoint struct { + Datapoints map[string]*Datapoint `json:"-"` + Uuid uuid.UUID `json:"uuid"` + Path string `json:"path"` + Value any `json:"value,omitempty"` + CreateDateTime int64 `json:"createDateTime,omitempty"` + UpdateDateTime int64 `json:"updateDateTime,omitempty"` + Type Type `json:"type"` + ReadWrite Rights `json:"readWrite"` + Drivers Drivers `json:"drivers,omitempty"` + Subscriptions Subscriptions `json:"-"` +} + +func (d *Datapoint) Set(path string, set Set) (bool, error) { + var changed bool + if path != "" { + changed = true + d.Path = path + } + + if set.Type != nil { + changed = true + d.Type = *set.Type + } + + if d.Type != "" { + if d.Value == nil && set.Value == nil { + changed = true + d.Value = d.Type.DefaultValue() + } else if d.Value != d.Type.ConvertValue(set.Value) { + changed = true + d.Value = d.Type.ConvertValue(set.Value) + } + + } + if set.Rights != nil { + changed = true + d.ReadWrite = set.Rights.GetRights() + } + + if changed { + d.UpdateDateTime = time.Now().UnixMilli() + } + + if set.Driver == nil { + return changed, nil + } + if set.Driver.Type == "" { + return changed, errors.New("driver type missing") + } + if set.Driver.Bus == "" { + return changed, errors.New("driver bus name missing") + } + + if d.Drivers == nil { + d.Drivers = NewDrivers() + } + d.Drivers.AddDriver(set.Driver.Type, set.Driver.Bus, set.Driver.Address) + d.UpdateDateTime = time.Now().UnixMilli() + + return changed, nil +} + +func (d *Datapoint) GetValueUint64() uint64 { + return utils.Uint64From(d.Value) +} + +func (d *Datapoint) CreateDatapoints(conns *serverModels.Connections, sets ...Set) (created []Set, err error) { + if len(sets) == 0 { + return + } + for _, dp := range sets { + parts := strings.Split(dp.Path, ":") + + current := d + for i, part := range parts { + if current.Datapoints == nil { + current.Datapoints = make(map[string]*Datapoint) + } + + if i == len(parts)-1 { + // Leaf node: create or update datapoint + if existing, ok := current.Datapoints[part]; ok { + publish, err := existing.Set("", dp) + if err != nil { + return nil, err + } + created = append(created, Set{ + Path: existing.Path, + Type: &existing.Type, + Value: existing.Value, + Rights: &existing.ReadWrite, + Drivers: &existing.Drivers, + Updated: true, + }) + + if publish { + existing.Publish(conns, OnChange) + } + } else { + // Create new + current.Datapoints[part] = &Datapoint{ + Uuid: uuid.New(), + CreateDateTime: time.Now().UnixMilli(), + Subscriptions: InitSubscribtion(), + } + publish, err := current.Datapoints[part].Set(strings.Join(parts, ":"), dp) + if err != nil { + return nil, err + } + created = append(created, Set{ + Path: current.Path, + Type: ¤t.Type, + Value: current.Value, + Rights: ¤t.ReadWrite, + Driver: dp.Driver, + }) + if publish { + current.Publish(conns, OnChange) + } + } + return + } + + // Traverse or create intermediate datapoints + if next, ok := current.Datapoints[part]; ok { + current = next + } else { + newDp := &Datapoint{ + Uuid: uuid.New(), + Path: strings.Join(parts[:i+1], ":"), + Type: NONE, + CreateDateTime: time.Now().UnixMilli(), + UpdateDateTime: time.Now().UnixMilli(), + Subscriptions: InitSubscribtion(), + } + + if dp.Rights != nil { + newDp.ReadWrite = dp.Rights.GetRights() + } + + current.Datapoints[part] = newDp + current = newDp + } + } + } + return +} + +func (d *Datapoint) ImportDatapoint(conns *serverModels.Connections, dp Datapoint, path string) error { + parts := strings.Split(dp.Path, ":") + + current := d + for i, part := range parts { + if current.Datapoints == nil { + current.Datapoints = make(map[string]*Datapoint) + } + + if i == len(parts)-1 { + // Leaf node: import the datapoint + if existing, ok := current.Datapoints[part]; ok { + existing.Type = dp.Type + existing.Value = current.Type.ConvertValue(dp.Value) + existing.ReadWrite = dp.ReadWrite.GetRights() + existing.UpdateDateTime = time.Now().UnixMilli() + dp.Publish(conns, OnChange) + } else { + dp.Path = strings.Join(parts, ":") + dp.ReadWrite = dp.ReadWrite.GetRights() + dp.UpdateDateTime = time.Now().UnixMilli() + dp.Subscriptions = InitSubscribtion() + current.Datapoints[part] = &dp + dp.Publish(conns, OnChange) + } + return nil + } + + // Traverse or create intermediate nodes + if next, ok := current.Datapoints[part]; ok { + current = next + } else { + newDp := &Datapoint{ + Path: strings.Join(parts[:i+1], ":"), + Type: NONE, + ReadWrite: dp.ReadWrite.GetRights(), + UpdateDateTime: time.Now().UnixMilli(), + } + newDp.ReadWrite = newDp.ReadWrite.GetRights() + current.Datapoints[part] = newDp + current = newDp + } + } + return nil +} + +func (d *Datapoint) UpdateDatapointValue(conns *serverModels.Connections, value any, path string) error { + + paths := strings.Split(path, ":") + + current := d + for i, part := range paths { + dp, ok := current.Datapoints[part] + if !ok { + return fmt.Errorf("datapoint path not found: %s (at %s)", path, part) + } + if i == len(paths)-1 { + dp.Value = dp.Type.ConvertValue(value) + dp.UpdateDateTime = time.Now().UnixMilli() + dp.Publish(conns, OnChange) + return nil + } + current = dp + } + return nil +} + +func (d *Datapoint) RemoveDatapoint(conns *serverModels.Connections, set Set) (Set, error) { + parts := strings.Split(set.Path, ":") + + if len(parts) < 1 { + return Set{}, fmt.Errorf("invalid path: '%s'", set.Path) + } + + current := d + for i := range len(parts) - 1 { + next, ok := current.Datapoints[parts[i]] + if !ok { + return Set{}, fmt.Errorf("path not found: '%s'", strings.Join(parts[:i+1], ":")) + } + current = next + } + + toDelete := parts[len(parts)-1] + if dp, ok := current.Datapoints[toDelete]; ok { + dp.Publish(conns, OnDelete) + delete(current.Datapoints, toDelete) + return Set{ + Path: set.Path, + }, nil + } + return Set{}, fmt.Errorf("datapoint '%s' not found", set.Path) +} + +func (d *Datapoint) GetAllDatapoints(depth int) (dps []*Datapoint) { + + var dfs func(dp *Datapoint, currentDepth int) + dfs = func(dp *Datapoint, currentDepth int) { + if depth == 1 { + return + } else if depth == 0 { + // Return all descendants + for _, child := range dp.Datapoints { + dps = append(dps, child) + dfs(child, currentDepth+1) + } + return + } + + if currentDepth == depth-1 { + return + } + + for _, child := range dp.Datapoints { + dps = append(dps, child) + dfs(child, currentDepth+1) + } + } + dps = append(dps, d) + dfs(d, 0) + return +} + +func (d *Datapoint) QueryDatapoints(depth int, path string) (dps []*Datapoint) { + parts := strings.Split(path, ":") + + var dfs func(current *Datapoint, index int) + dfs = func(current *Datapoint, index int) { + + if index == len(parts) { + dps = append(dps, current.GetAllDatapoints(depth)...) + return + } + + pattern := "^" + parts[index] + "$" + re, err := regexp.Compile(pattern) + if err != nil { + return + } + + for name, dp := range current.Datapoints { + if re.MatchString(name) { + dfs(dp, index+1) + } + } + } + + dfs(d, 0) + return +} + +func (d *Datapoint) AddSubscribtion(id string, sub *Subscribe) { + if d.Subscriptions == nil { + return + } + + if s, ok := d.Subscriptions[id]; ok { + s.OnCreate = sub.GetOnCreate() + s.OnChange = sub.GetOnChange() + s.OnDelete = sub.GetOnDelete() + } else { + d.Subscriptions[id] = &Subscription{ + OnCreate: sub.GetOnCreate(), + OnChange: sub.GetOnChange(), + OnDelete: sub.GetOnDelete(), + } + } +} + +func (d *Datapoint) RemoveSubscribtion(id string) { + if _, ok := d.Subscriptions[id]; !ok { + return + } + delete(d.Subscriptions, id) +} + +func (d *Datapoint) Publish(conns *serverModels.Connections, eventType string) error { + if conns.Clients == nil { + return nil + } + conns.RLock() + defer conns.RUnlock() + + for id := range d.Subscriptions { + if client, ok := conns.Clients[id]; !ok { + delete(d.Subscriptions, id) + } else { + err := wsjson.Write(client.Ctx, client.Conn, Publish{ + Event: eventType, + Path: d.Path, + Value: d.Value, + }) + if err != nil { + return err + } + } + } + return nil +} diff --git a/drivers.go b/drivers.go new file mode 100644 index 0000000..c1cb37c --- /dev/null +++ b/drivers.go @@ -0,0 +1,20 @@ +package models + +type Drivers map[string]*Driver + +type Driver struct { + Type string `json:"type,omitempty"` + Bus string `json:"bus"` + Address int `json:"address"` +} + +func NewDrivers() Drivers { + return make(Drivers) +} + +func (d *Drivers) AddDriver(typ, bus string, address int) { + (*d)[typ] = &Driver{ + Bus: bus, + Address: address, + } +} diff --git a/get.go b/get.go new file mode 100644 index 0000000..43c68ba --- /dev/null +++ b/get.go @@ -0,0 +1,13 @@ +package models + +import "github.com/google/uuid" + +type Get struct { + Uuid uuid.UUID `json:"uuid"` + Path string `json:"path"` + Query *Query `json:"query,omitempty"` + Drivers Drivers `json:"driver,omitempty"` + Type Type `json:"type,omitempty"` + Value any `json:"value,omitempty"` + Rights Rights `json:"rights,omitempty"` +} diff --git a/jsonData.go b/jsonData.go new file mode 100644 index 0000000..c7b1955 --- /dev/null +++ b/jsonData.go @@ -0,0 +1,37 @@ +package models + +type JsonData struct { + Get *[]Get `json:"get,omitempty"` + Set *[]Set `json:"set,omitempty"` + Subscribe *[]Subscribe `json:"subscribe,omitempty"` + Subscribed *[]Subscribed `json:"subscribed,omitempty"` + Unsubscribe *[]Subscribe `json:"unsubscribe,omitempty"` + Publish *[]Publish `json:"publish,omitempty"` +} + +func NewRequest() *JsonData { + return &JsonData{} + +} + +func (r *JsonData) AddGet(path string, query Query) { + if r.Get == nil { + r.Get = &[]Get{} + } + + *r.Get = append(*r.Get, Get{ + Path: path, + Query: &query, + }) +} + +func (r *JsonData) AddSet(path string, value any, create bool) { + if r.Set == nil { + r.Set = &[]Set{} + } + + *r.Set = append(*r.Set, Set{ + Path: path, + Value: value, + }) +} diff --git a/jsonResponse.go b/jsonResponse.go new file mode 100644 index 0000000..c9d7cc3 --- /dev/null +++ b/jsonResponse.go @@ -0,0 +1,13 @@ +package models + +type JsonResponse struct { + Error *bool `json:"error,omitempty"` + Message string `json:"message,omitempty"` + Data string `json:"data,omitempty"` + Event string `json:"event,omitempty"` + Path string `json:"path,omitempty"` + Value any `json:"value,omitempty"` + Set *[]Set `json:"set,omitempty"` + Subscribe *[]Subscribe `json:"subscribe,omitempty"` + Unubscribe *[]Subscribe `json:"unsubscribe,omitempty"` +} diff --git a/port.go b/port.go new file mode 100644 index 0000000..5394e13 --- /dev/null +++ b/port.go @@ -0,0 +1,6 @@ +package models + +type Port struct { + Http uint + Https uint +} diff --git a/publish.go b/publish.go new file mode 100644 index 0000000..af35d41 --- /dev/null +++ b/publish.go @@ -0,0 +1,11 @@ +package models + +import "github.com/google/uuid" + +type Publish struct { + Event string `json:"event,omitempty"` + Uuid uuid.UUID `json:"uuid,omitempty"` + Path string `json:"path,omitempty"` + Value any `json:"value,omitempty"` + Driver *Driver `json:"driver,omitempty"` +} diff --git a/query.go b/query.go new file mode 100644 index 0000000..2a06e93 --- /dev/null +++ b/query.go @@ -0,0 +1,6 @@ +package models + +type Query struct { + Depth int `json:"depth,omitempty"` + RegExp string `json:"regExp,omitempty"` +} diff --git a/rights.go b/rights.go new file mode 100644 index 0000000..043a69c --- /dev/null +++ b/rights.go @@ -0,0 +1,18 @@ +package models + +type Rights string + +const ( + Read Rights = "R" + Write Rights = "W" + ReadWrite Rights = "RW" +) + +func (r *Rights) GetRights() Rights { + if r == nil { + return ReadWrite + } else if *r == "" { + return ReadWrite + } + return *r +} diff --git a/set.go b/set.go new file mode 100644 index 0000000..0e4a2d1 --- /dev/null +++ b/set.go @@ -0,0 +1,12 @@ +package models + +type Set struct { + Path string `json:"path"` + Driver *Driver `json:"driver,omitempty"` + Drivers *Drivers `json:"drivers,omitempty"` + Type *Type `json:"type,omitempty"` + Value any `json:"value,omitempty"` + Rights *Rights `json:"rights,omitempty"` + Create bool `json:"create,omitempty"` + Updated bool `json:"-"` +} diff --git a/subscribe.go b/subscribe.go new file mode 100644 index 0000000..826f544 --- /dev/null +++ b/subscribe.go @@ -0,0 +1,38 @@ +package models + +type Subscribe struct { + Path string `json:"path"` + Depth *int `json:"depth,omitempty"` + Driver *string `json:"driver,omitempty"` + OnCreate *bool `json:"onCreate,omitempty"` + OnDelete *bool `json:"onDelete,omitempty"` + OnChange *bool `json:"onChange,omitempty"` +} + +func (s *Subscribe) GetDepth() int { + if s.Depth == nil { + return 0 + } + return *s.Depth +} + +func (s *Subscribe) GetOnCreate() bool { + if s.OnCreate == nil { + return false + } + return *s.OnCreate +} + +func (s *Subscribe) GetOnChange() bool { + if s.OnChange == nil { + return false + } + return *s.OnChange +} + +func (s *Subscribe) GetOnDelete() bool { + if s.OnDelete == nil { + return false + } + return *s.OnDelete +} diff --git a/subscribed.go b/subscribed.go new file mode 100644 index 0000000..bcd7138 --- /dev/null +++ b/subscribed.go @@ -0,0 +1,9 @@ +package models + +import "github.com/google/uuid" + +type Subscribed struct { + Uuid uuid.UUID `json:"uuid"` + Path string `json:"path"` + Driver *Driver `json:"driver,omitempty"` +} diff --git a/subscribtion.go b/subscribtion.go new file mode 100644 index 0000000..f8542e5 --- /dev/null +++ b/subscribtion.go @@ -0,0 +1,13 @@ +package models + +type Subscriptions map[string]*Subscription + +type Subscription struct { + OnCreate bool + OnDelete bool + OnChange bool +} + +func InitSubscribtion() Subscriptions { + return make(Subscriptions) +} diff --git a/type.go b/type.go new file mode 100644 index 0000000..81f4171 --- /dev/null +++ b/type.go @@ -0,0 +1,102 @@ +package models + +import ( + "fmt" + "math/rand" + + "github.com/tecamino/tecamino-dbm/utils" +) + +const ( + NONE Type = "NONE" + BIT Type = "BIT" + BYU Type = "BYU" // UINT8 + BYS Type = "BYS" // INT8 + WOS Type = "WOS" // INT16 + WOU Type = "WOU" // UINT16 + DWS Type = "DWS" // INT32 + DWU Type = "DWU" // UINT32 + LOS Type = "LOS" // INT64 + LOU Type = "LOU" // UINT64 + F32 Type = "F32" // FLOAT32 + F64 Type = "F64" // FLOAT64 + STR Type = "STRING" // STRING +) + +type Type string + +func (t *Type) DefaultValue() any { + switch *t { + case BIT: + return false + case BYS, BYU, WOS, WOU, DWS, DWU, LOS, LOU, F32, F64: + return 0 + case STR: + return "" + } + return nil +} + +func (t *Type) ConvertValue(v any) any { + switch *t { + case BIT: + return utils.BoolFrom(v) + case BYS: + return utils.Int8From(v) + case BYU: + return utils.Uint8From(v) + case WOS: + return utils.Int16From(v) + case WOU: + return utils.Uint16From(v) + case DWS: + return utils.Int32From(v) + case DWU: + return utils.Uint32From(v) + case LOS: + return utils.Int64From(v) + case LOU: + return utils.Uint64From(v) + case F32: + return utils.Float32From(v) + case F64: + return utils.Float64From(v) + case STR: + return fmt.Sprintf("%v", v) + } + return nil +} + +func RandomType() Type { + n := rand.Intn(11) + 1 + + switch n { + case 1: + return "BIT" + case 2: + return "BYU" + case 3: + return "BYS" + case 4: + return "WOS" + case 5: + return "WOU" + case 6: + return "DWS" + case 7: + return "DWU" + case 8: + return "LOS" + case 9: + return "LOU" + case 10: + return "F32" + case 11: + return "F64" + case 12: + return "STRING" + default: + return "NONE" + } + +}