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" + } + +}