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 }