github.com/simpleiot/simpleiot@v0.18.3/data/point.go (about)

     1  package data
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/binary"
     6  	"fmt"
     7  	"hash/crc32"
     8  	"math"
     9  	"time"
    10  
    11  	"github.com/golang/protobuf/ptypes"
    12  	"github.com/simpleiot/simpleiot/internal/pb"
    13  	"google.golang.org/protobuf/proto"
    14  )
    15  
    16  // Point is a flexible data structure that can be used to represent
    17  // a sensor value or a configuration parameter.
    18  // Type, and Key uniquely identify a point in a node.
    19  // Using the Key field, maps and arrays can be represented.
    20  // Array would have key values like: "0", "1", "2", "3", ...
    21  // A map might have key values like "min", "max", "average", etc.
    22  type Point struct {
    23  	//-------------------------------------------------------
    24  	//1st three fields uniquely identify a point when receiving updates
    25  
    26  	// Type of point (voltage, current, key, etc)
    27  	Type string `json:"type,omitempty"`
    28  
    29  	// Key is used to allow a group of points to represent a map or array
    30  	Key string `json:"key,omitempty"`
    31  
    32  	//-------------------------------------------------------
    33  	// The following fields are the values for a point
    34  
    35  	// Time the point was taken
    36  	Time time.Time `json:"time,omitempty" yaml:"-"`
    37  
    38  	// Instantaneous analog or digital value of the point.
    39  	// 0 and 1 are used to represent digital values
    40  	Value float64 `json:"value,omitempty"`
    41  
    42  	// Optional text value of the point for data that is best represented
    43  	// as a string rather than a number.
    44  	Text string `json:"text,omitempty"`
    45  
    46  	// catchall field for data that does not fit into float or string --
    47  	// should be used sparingly
    48  	Data []byte `json:"data,omitempty"`
    49  
    50  	//-------------------------------------------------------
    51  	// Metadata
    52  
    53  	// Used to indicate a point has been deleted. This value is only
    54  	// ever incremented. Odd values mean point is deleted.
    55  	Tombstone int `json:"tombstone,omitempty"`
    56  
    57  	// Where did this point come from. If from the owning node, it may be blank.
    58  	Origin string `json:"origin,omitempty"`
    59  }
    60  
    61  // CRC returns a CRC for the point
    62  func (p Point) CRC() uint32 {
    63  	// Node type points are not returned so don't include that in hash
    64  	if p.Type == PointTypeNodeType {
    65  		return 0
    66  	}
    67  	// we are using this in a XOR checksum, so simply hashing time is probably
    68  	// not good enough, because if we send a bunch of points with the same time,
    69  	// they will have the CRC and simply cancel each other out.
    70  	h := crc32.NewIEEE()
    71  	d := make([]byte, 8)
    72  	binary.LittleEndian.PutUint64(d, uint64(p.Time.UnixNano()))
    73  	h.Write(d)
    74  	h.Write([]byte(p.Type))
    75  	h.Write([]byte(p.Key))
    76  	h.Write([]byte(p.Text))
    77  	binary.LittleEndian.PutUint64(d, math.Float64bits(p.Value))
    78  	h.Write(d)
    79  
    80  	return h.Sum32()
    81  }
    82  
    83  func (p Point) String() string {
    84  	t := ""
    85  
    86  	if p.Type != "" {
    87  		t += "T:" + p.Type + " "
    88  	}
    89  
    90  	if p.Text != "" {
    91  		t += fmt.Sprintf("V:%v ", p.Text)
    92  	} else {
    93  		t += fmt.Sprintf("V:%.3f ", p.Value)
    94  	}
    95  
    96  	if p.Key != "" && p.Key != "0" {
    97  		t += fmt.Sprintf("K:%v ", p.Key)
    98  	}
    99  
   100  	if p.Origin != "" {
   101  		t += fmt.Sprintf("O:%v ", p.Origin)
   102  	}
   103  
   104  	if p.Tombstone != 0 {
   105  		t += "Tomb "
   106  	}
   107  
   108  	if !p.Time.IsZero() {
   109  		t += p.Time.Format(time.RFC3339)
   110  	}
   111  
   112  	return t
   113  }
   114  
   115  // IsMatch returns true if the point matches the params passed in
   116  func (p Point) IsMatch(typ, key string) bool {
   117  	if typ != "" && typ != p.Type {
   118  		return false
   119  	}
   120  
   121  	if key != p.Key {
   122  		return false
   123  	}
   124  
   125  	return true
   126  }
   127  
   128  // ToPb encodes point in protobuf format
   129  func (p Point) ToPb() (pb.Point, error) {
   130  	ts, err := ptypes.TimestampProto(p.Time)
   131  	if err != nil {
   132  		return pb.Point{}, err
   133  	}
   134  
   135  	return pb.Point{
   136  		Type:      p.Type,
   137  		Key:       p.Key,
   138  		Value:     p.Value,
   139  		Text:      p.Text,
   140  		Time:      ts,
   141  		Tombstone: int32(p.Tombstone),
   142  		Origin:    p.Origin,
   143  	}, nil
   144  }
   145  
   146  // ToSerial encodes point in serial protobuf format
   147  func (p Point) ToSerial() (pb.SerialPoint, error) {
   148  	return pb.SerialPoint{
   149  		Type:      p.Type,
   150  		Key:       p.Key,
   151  		Value:     float32(p.Value),
   152  		Text:      p.Text,
   153  		Time:      p.Time.UnixNano(),
   154  		Tombstone: int32(p.Tombstone),
   155  		Origin:    p.Origin,
   156  	}, nil
   157  }
   158  
   159  // Bool returns a bool representation of value
   160  func (p *Point) Bool() bool {
   161  	return p.Value == 1
   162  }
   163  
   164  // Points is an array of Point
   165  type Points []Point
   166  
   167  func (ps Points) String() string {
   168  	ret := ""
   169  	for _, p := range ps {
   170  		ret += p.String() + "\n"
   171  	}
   172  
   173  	return ret
   174  }
   175  
   176  // Desc returns a Description of a set of points
   177  func (ps Points) Desc() string {
   178  	firstName, _ := ps.Text(PointTypeFirstName, "")
   179  	if firstName != "" {
   180  		lastName, _ := ps.Text(PointTypeLastName, "")
   181  		if lastName == "" {
   182  			return firstName
   183  		}
   184  
   185  		return firstName + " " + lastName
   186  	}
   187  
   188  	desc, _ := ps.Text(PointTypeDescription, "")
   189  	if desc != "" {
   190  		return desc
   191  	}
   192  
   193  	return ""
   194  }
   195  
   196  // Find fetches a point given ID, Type, and Index
   197  // and true of found, or false if not found
   198  func (ps Points) Find(typ, key string) (Point, bool) {
   199  	if key == "" {
   200  		key = "0"
   201  	}
   202  	for _, p := range ps {
   203  		if !p.IsMatch(typ, key) {
   204  			continue
   205  		}
   206  
   207  		return p, true
   208  	}
   209  
   210  	return Point{}, false
   211  }
   212  
   213  // Value fetches a value from an array of points given ID, Type, and Index.
   214  // If ID or Type are set to "", they are ignored.
   215  func (ps *Points) Value(typ, key string) (float64, bool) {
   216  	p, ok := ps.Find(typ, key)
   217  	return p.Value, ok
   218  }
   219  
   220  // ValueInt returns value as integer
   221  func (ps *Points) ValueInt(typ, key string) (int, bool) {
   222  	f, ok := ps.Value(typ, key)
   223  	return int(f), ok
   224  }
   225  
   226  // ValueBool returns value as bool
   227  func (ps *Points) ValueBool(typ, key string) (bool, bool) {
   228  	f, ok := ps.Value(typ, key)
   229  	return FloatToBool(f), ok
   230  }
   231  
   232  // Text fetches a text value from an array of points given Type and Key.
   233  // If ID or Type are set to "", they are ignored.
   234  func (ps *Points) Text(typ, key string) (string, bool) {
   235  	p, ok := ps.Find(typ, key)
   236  	return p.Text, ok
   237  }
   238  
   239  // LatestTime returns the latest timestamp of a devices points
   240  func (ps *Points) LatestTime() time.Time {
   241  	ret := time.Time{}
   242  	for _, p := range *ps {
   243  		if p.Time.After(ret) {
   244  			ret = p.Time
   245  		}
   246  	}
   247  
   248  	return ret
   249  }
   250  
   251  // ToPb encodes an array of points into protobuf
   252  func (ps *Points) ToPb() ([]byte, error) {
   253  	pbPoints := make([]*pb.Point, len(*ps))
   254  	for i, s := range *ps {
   255  		sPb, err := s.ToPb()
   256  		if err != nil {
   257  			return []byte{}, err
   258  		}
   259  
   260  		pbPoints[i] = &sPb
   261  	}
   262  
   263  	return proto.Marshal(&pb.Points{Points: pbPoints})
   264  }
   265  
   266  // question -- should be using []*Point instead of []Point?
   267  
   268  // Hash returns the hash of points
   269  func (ps *Points) Hash() uint32 {
   270  	var ret uint32
   271  
   272  	for _, p := range *ps {
   273  		ret = ret ^ p.CRC()
   274  	}
   275  
   276  	return ret
   277  }
   278  
   279  // Add takes a point and updates an existing array of points. Existing points
   280  // are replaced if the Timestamp in pIn is > than the existing timestamp. If
   281  // the pIn timestamp is zero, the current time is used.
   282  func (ps *Points) Add(pIn Point) {
   283  	pFound := false
   284  
   285  	if pIn.Key == "" {
   286  		pIn.Key = "0"
   287  	}
   288  
   289  	if pIn.Time.IsZero() {
   290  		pIn.Time = time.Now()
   291  	}
   292  
   293  	for i, p := range *ps {
   294  		if p.Key == pIn.Key && p.Type == pIn.Type {
   295  			pFound = true
   296  			// largest tombstone value always wins
   297  			tombstone := p.Tombstone
   298  			if pIn.Tombstone > p.Tombstone {
   299  				tombstone = pIn.Tombstone
   300  			}
   301  
   302  			if pIn.Time.After(p.Time) {
   303  				(*ps)[i] = pIn
   304  			}
   305  			(*ps)[i].Tombstone = tombstone
   306  			break
   307  		}
   308  	}
   309  
   310  	if !pFound {
   311  		*ps = append(*ps, pIn)
   312  	}
   313  }
   314  
   315  // Merge is used to update points. Any points that are changed
   316  // are returned. maxDuration can be used to return points
   317  // if they have not been updated in maxDuration -- this can
   318  // be used to send out points every X duration even if they
   319  // are not changing which is useful for making graphs look
   320  // nice. Set maxTime to zero to disable.
   321  func (ps *Points) Merge(in Points, maxTime time.Duration) Points {
   322  	var ret Points
   323  
   324  	for _, pIn := range in {
   325  		pFound := false
   326  		modified := false
   327  		if pIn.Time.IsZero() {
   328  			pIn.Time = time.Now()
   329  		}
   330  
   331  		for i, p := range *ps {
   332  			if p.Key == pIn.Key && p.Type == pIn.Type {
   333  				pFound = true
   334  				// largest tombstone value always wins
   335  				if pIn.Tombstone > p.Tombstone {
   336  					(*ps)[i].Tombstone = pIn.Tombstone
   337  					modified = true
   338  				}
   339  
   340  				if !pIn.Time.After(p.Time) {
   341  					break
   342  				}
   343  
   344  				if pIn.Value != p.Value {
   345  					(*ps)[i] = p
   346  					modified = true
   347  				}
   348  
   349  				if maxTime > 0 && pIn.Time.Sub(p.Time) > maxTime {
   350  					(*ps)[i] = p
   351  					modified = true
   352  				}
   353  
   354  				if pIn.Text != p.Text {
   355  					(*ps)[i] = p
   356  					modified = true
   357  				}
   358  
   359  				(*ps)[i] = pIn
   360  			}
   361  		}
   362  
   363  		if !pFound {
   364  			*ps = append(*ps, pIn)
   365  			modified = true
   366  		}
   367  
   368  		if modified {
   369  			ret = append(ret, pIn)
   370  		}
   371  	}
   372  
   373  	return ret
   374  }
   375  
   376  // Collapse is used to merge any common points and keep the latest
   377  func (ps *Points) Collapse() {
   378  	if len(*ps) <= 1 {
   379  		return
   380  	}
   381  
   382  	pts := make(map[string]Point)
   383  
   384  	for _, p := range *ps {
   385  		pA, OK := pts[p.Type+p.Key]
   386  		if OK {
   387  			if pA.Time.Before(p.Time) || pA.Time.Equal(p.Time) {
   388  				pts[p.Type+p.Key] = p
   389  			}
   390  		} else {
   391  			pts[p.Type+p.Key] = p
   392  		}
   393  	}
   394  
   395  	*ps = make(Points, len(pts))
   396  	i := 0
   397  	for _, p := range pts {
   398  		(*ps)[i] = p
   399  		i++
   400  	}
   401  }
   402  
   403  // Implement methods needed by sort.Interface
   404  
   405  // Len returns the number of points
   406  func (ps Points) Len() int {
   407  	return len([]Point(ps))
   408  }
   409  
   410  // Less is required by sort.Interface
   411  func (ps Points) Less(i, j int) bool {
   412  	return ps[i].Time.Before(ps[j].Time)
   413  }
   414  
   415  // Swap is required by sort.Interface
   416  func (ps Points) Swap(i, j int) {
   417  	ps[i], ps[j] = ps[j], ps[i]
   418  }
   419  
   420  // ByTypeKey can be used to sort points by type then key
   421  type ByTypeKey []Point
   422  
   423  func (b ByTypeKey) Len() int      { return len(b) }
   424  func (b ByTypeKey) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
   425  func (b ByTypeKey) Less(i, j int) bool {
   426  	if b[i].Type != b[j].Type {
   427  		return b[i].Type < b[j].Type
   428  	}
   429  
   430  	return b[i].Key < b[j].Key
   431  }
   432  
   433  // PbToPoint converts pb point to point
   434  func PbToPoint(sPb *pb.Point) (Point, error) {
   435  
   436  	ts, err := ptypes.Timestamp(sPb.Time)
   437  	if err != nil {
   438  		return Point{}, err
   439  	}
   440  
   441  	ret := Point{
   442  		Type:      sPb.Type,
   443  		Text:      sPb.Text,
   444  		Key:       sPb.Key,
   445  		Value:     sPb.Value,
   446  		Time:      ts,
   447  		Tombstone: int(sPb.Tombstone),
   448  		Origin:    sPb.Origin,
   449  	}
   450  
   451  	return ret, nil
   452  }
   453  
   454  // SerialToPoint converts serial pb point to point
   455  func SerialToPoint(sPb *pb.SerialPoint) (Point, error) {
   456  	ret := Point{
   457  		Type:      sPb.Type,
   458  		Text:      sPb.Text,
   459  		Key:       sPb.Key,
   460  		Value:     float64(sPb.Value),
   461  		Time:      time.Unix(0, sPb.Time),
   462  		Tombstone: int(sPb.Tombstone),
   463  		Origin:    sPb.Origin,
   464  	}
   465  
   466  	return ret, nil
   467  }
   468  
   469  // PbDecodePoints decode protobuf encoded points
   470  func PbDecodePoints(data []byte) (Points, error) {
   471  	pbPoints := &pb.Points{}
   472  	err := proto.Unmarshal(data, pbPoints)
   473  	if err != nil {
   474  		return []Point{}, err
   475  	}
   476  
   477  	ret := make([]Point, len(pbPoints.Points))
   478  
   479  	for i, sPb := range pbPoints.Points {
   480  		s, err := PbToPoint(sPb)
   481  		if err != nil {
   482  			return []Point{}, err
   483  		}
   484  		ret[i] = s
   485  	}
   486  
   487  	return ret, nil
   488  }
   489  
   490  // DecodeSerialHrPayload decodes a serial high-rate payload. Payload format.
   491  //   - type         (off:0, 16 bytes) point type
   492  //   - key          (off:16, 16 bytes) point key
   493  //   - starttime    (off:32, uint64) starting time of samples in ns since Unix Epoch
   494  //   - sampleperiod (off:40, uint32) time between samples in ns
   495  //   - data         (off:44) packed 32-bit floating point samples
   496  func DecodeSerialHrPayload(payload []byte, callback func(Point)) error {
   497  	if len(payload) < 16+16+8+4+4 {
   498  		return fmt.Errorf("Payload is not long enough")
   499  	}
   500  
   501  	typ := string(bytes.Trim(payload[0:16], "\x00"))
   502  	key := string(bytes.Trim(payload[16:32], "\x00"))
   503  	startNs := int64(binary.LittleEndian.Uint64(payload[32:40]))
   504  	if startNs == 0 {
   505  		// if MCU does not send a time, fill in current time
   506  		startNs = time.Now().UnixNano()
   507  	}
   508  	sampNs := int64(binary.LittleEndian.Uint32(payload[40:44]))
   509  
   510  	// FIXME, this API should not use a callback for each
   511  	// point, that is probably why it is so slow
   512  
   513  	sampCount := (len(payload) - (16 + 16 + 8 + 4)) / 4
   514  	for i := 0; i < sampCount; i++ {
   515  		callback(Point{
   516  			Time: time.Unix(0, startNs+int64(i)*sampNs),
   517  			Type: typ,
   518  			Key:  key,
   519  			Value: float64(math.Float32frombits(
   520  				binary.LittleEndian.Uint32(payload[44+i*4 : 44+4+i*4]))),
   521  		})
   522  	}
   523  
   524  	return nil
   525  }
   526  
   527  // PbDecodeSerialPoints can be used to decode serial points
   528  func PbDecodeSerialPoints(d []byte) (Points, error) {
   529  	pbSerial := &pb.SerialPoints{}
   530  
   531  	err := proto.Unmarshal(d, pbSerial)
   532  	if err != nil {
   533  		return nil, fmt.Errorf("PB decode error: %v", err)
   534  	}
   535  
   536  	points := make([]Point, len(pbSerial.Points))
   537  
   538  	for i, sPb := range pbSerial.Points {
   539  		s, err := SerialToPoint(sPb)
   540  		if err != nil {
   541  			return nil, fmt.Errorf("Point decode error: %v", err)
   542  		}
   543  		points[i] = s
   544  	}
   545  
   546  	return points, nil
   547  }
   548  
   549  // PointFilter is used to send points upstream. It only sends
   550  // the data has changed, and at a max frequency
   551  type PointFilter struct {
   552  	minSend          time.Duration
   553  	periodicSend     time.Duration
   554  	points           []Point
   555  	lastSent         time.Time
   556  	lastPeriodicSend time.Time
   557  }
   558  
   559  // NewPointFilter is used to creat a new point filter
   560  // If points have changed that get sent out at a minSend interval
   561  // frequency of minSend.
   562  // All points are periodically sent at lastPeriodicSend interval.
   563  // Set minSend to 0 for things like config settings where you want them
   564  // to be sent whenever anything changes.
   565  func NewPointFilter(minSend, periodicSend time.Duration) *PointFilter {
   566  	return &PointFilter{
   567  		minSend:      minSend,
   568  		periodicSend: periodicSend,
   569  	}
   570  }
   571  
   572  // returns true if point has changed, and merges point with saved points
   573  func (sf *PointFilter) add(point Point) bool {
   574  	for i, p := range sf.points {
   575  		if point.Key == p.Key &&
   576  			point.Type == p.Type {
   577  			if point.Value == p.Value {
   578  				return false
   579  			}
   580  
   581  			sf.points[i].Value = point.Value
   582  			return true
   583  		}
   584  	}
   585  
   586  	// point not found, add to array
   587  	sf.points = append(sf.points, point)
   588  	return true
   589  }
   590  
   591  // Add adds points and returns points that meet the filter criteria
   592  func (sf *PointFilter) Add(points []Point) []Point {
   593  	if time.Since(sf.lastPeriodicSend) > sf.periodicSend {
   594  		// send all points
   595  		for _, s := range points {
   596  			sf.add(s)
   597  		}
   598  
   599  		sf.lastPeriodicSend = time.Now()
   600  		sf.lastSent = sf.lastPeriodicSend
   601  		return sf.points
   602  	}
   603  
   604  	if sf.minSend != 0 && time.Since(sf.lastSent) < sf.minSend {
   605  		// don't return anything as
   606  		return []Point{}
   607  	}
   608  
   609  	// now check if anything has changed and just send what has changed
   610  	// only
   611  	var ret []Point
   612  
   613  	for _, s := range points {
   614  		if sf.add(s) {
   615  			ret = append(ret, s)
   616  		}
   617  	}
   618  
   619  	if len(ret) > 0 {
   620  		sf.lastSent = time.Now()
   621  	}
   622  
   623  	return ret
   624  }
   625  
   626  // FloatToBool converts a float to bool
   627  func FloatToBool(v float64) bool {
   628  	return v == 1
   629  }
   630  
   631  // BoolToFloat converts bool to float
   632  func BoolToFloat(v bool) float64 {
   633  	if !v {
   634  		return 0
   635  	}
   636  	return 1
   637  }