github.com/simpleiot/simpleiot@v0.18.3/client/signal-generator.go (about)

     1  package client
     2  
     3  import (
     4  	"log"
     5  	"math"
     6  	"math/rand"
     7  	"os"
     8  	"time"
     9  
    10  	"github.com/nats-io/nats.go"
    11  	"github.com/simpleiot/simpleiot/data"
    12  )
    13  
    14  // SignalGenerator config
    15  type SignalGenerator struct {
    16  	ID          string      `node:"id"`
    17  	Parent      string      `node:"parent"`
    18  	Description string      `point:"description"`
    19  	Disabled    bool        `point:"disabled"`
    20  	Destination Destination `point:"destination"`
    21  	Units       string      `point:"units"`
    22  	// SignalType must be one of: "sine", "square", "triangle", or "random walk"
    23  	SignalType string  `point:"signalType"`
    24  	MinValue   float64 `point:"minValue"`
    25  	MaxValue   float64 `point:"maxValue"`
    26  	// InitialValue is the starting value for the signal generator.
    27  	// For random walk, this must be between MinValue and MaxValue. For wave
    28  	// functions, this must be in radians (i.e. between 0 and 2 * Pi).
    29  	InitialValue float64 `point:"initialValue"`
    30  	RoundTo      float64 `point:"roundTo"`
    31  	// SampleRate in Hz.
    32  	SampleRate float64 `point:"sampleRate"`
    33  	// BatchPeriod is the batch timer interval in ms. When the timer fires, it
    34  	// generates a batch of points at the specified SampleRate. If not set,
    35  	// timer will fire for each sample at SampleRate.
    36  	BatchPeriod int `point:"batchPeriod"`
    37  	// Frequency for wave functions (in Hz.)
    38  	Frequency float64 `point:"frequency"`
    39  	// Min./Max. increment amount for random walk function
    40  	MinIncrement float64 `point:"minIncrement"`
    41  	MaxIncrement float64 `point:"maxIncrement"`
    42  	// Current value
    43  	Value float64           `point:"value"`
    44  	Tags  map[string]string `point:"tag"`
    45  }
    46  
    47  /* TODO: Optimization
    48  
    49  Note that future designs may keep track of all running SignalGeneratorClients
    50  and manage only a single batch period timer that runs at a scheduled time.
    51  One way to do this is to keep track of each clients' next scheduled run time in
    52  a sorted list. Then, we schedule the timer to run at the soonest scheduled time.
    53  At that time, we process all clients' batches of points and reschedule another
    54  timer.
    55  
    56  */
    57  
    58  // BatchSizeLimit is the largest number of points generated per batch.
    59  // If the number of points to be generated by a SignalGenerator exceed this
    60  // limit, the remaining points will be dropped and generated wave signals may
    61  // experience a phase shift.
    62  const BatchSizeLimit = 1000000
    63  
    64  // SignalGeneratorClient for signal generator nodes
    65  type SignalGeneratorClient struct {
    66  	log           *log.Logger
    67  	nc            *nats.Conn
    68  	config        SignalGenerator
    69  	stop          chan struct{}
    70  	newPoints     chan NewPoints
    71  	newEdgePoints chan NewPoints
    72  }
    73  
    74  // NewSignalGeneratorClient ...
    75  func NewSignalGeneratorClient(nc *nats.Conn, config SignalGenerator) Client {
    76  	return &SignalGeneratorClient{
    77  		log:           log.New(os.Stderr, "signalGenerator: ", log.LstdFlags|log.Lmsgprefix),
    78  		nc:            nc,
    79  		config:        config,
    80  		stop:          make(chan struct{}, 1),
    81  		newPoints:     make(chan NewPoints, 1),
    82  		newEdgePoints: make(chan NewPoints, 1),
    83  	}
    84  }
    85  
    86  // clamp clamps val to fall in the range [min, max].
    87  func clamp(val, min, max float64) float64 {
    88  	// Ensure range of val is [min, max]
    89  	if val < min {
    90  		return min
    91  	} else if val > max {
    92  		return max
    93  	}
    94  	return val
    95  }
    96  
    97  // round rounds val to the nearest to.
    98  // When `to` is 0.1, `val` is rounded to the nearest tenth, for example.
    99  // No rounding occurs if to <= 0
   100  func round(val, to float64) float64 {
   101  	if to > 0 {
   102  		return math.Round(val/to) * to
   103  	}
   104  	return val
   105  }
   106  
   107  // Run the main logic for this client and blocks until stopped
   108  func (sgc *SignalGeneratorClient) Run() error {
   109  	sgc.log.Printf("Starting client: %v", sgc.config.Description)
   110  
   111  	chStopGen := make(chan struct{})
   112  
   113  	generator := func(config SignalGenerator) {
   114  		configValid := true
   115  		amplitude := config.MaxValue - config.MinValue
   116  		lastValue := config.InitialValue
   117  
   118  		if config.Disabled {
   119  			sgc.log.Printf("%v: disabled\n", config.Description)
   120  			configValid = false
   121  		}
   122  
   123  		// Validate type
   124  		switch config.SignalType {
   125  		case "sine":
   126  			fallthrough
   127  		case "square":
   128  			fallthrough
   129  		case "triangle":
   130  			if config.Frequency <= 0 {
   131  				sgc.log.Printf("%v: Frequency must be set\n", config.Description)
   132  				configValid = false
   133  			}
   134  			// Note: lastValue is in radians; let's just sanitize it a bit
   135  			lastValue = math.Mod(lastValue, (2 * math.Pi))
   136  		case "random walk":
   137  			if config.MaxIncrement <= config.MinIncrement {
   138  				sgc.log.Printf("%v: MaxIncrement must be larger than MinIncrement\n", config.Description)
   139  				configValid = false
   140  			}
   141  			lastValue = clamp(config.InitialValue, config.MinValue, config.MaxValue)
   142  		default:
   143  			sgc.log.Printf("%v: Type %v is invalid\n", config.Description, config.SignalType)
   144  			configValid = false
   145  		}
   146  
   147  		if amplitude <= 0 {
   148  			sgc.log.Printf("%v: MaxValue %v must be larger than MinValue %v\n", config.Description, config.MaxValue, config.MinValue)
   149  			configValid = false
   150  		}
   151  
   152  		if config.SampleRate <= 0 {
   153  			sgc.log.Printf("%v: SampleRate must be set\n", config.Description)
   154  			configValid = false
   155  		}
   156  
   157  		if config.Destination.HighRate && config.BatchPeriod <= 0 {
   158  			sgc.log.Printf("%v: BatchPeriod must be set for high-rate data\n", config.Description)
   159  			configValid = false
   160  		}
   161  
   162  		natsSubject := config.Destination.Subject(config.ID, config.Parent)
   163  		pointType := data.PointTypeValue
   164  		if config.Destination.PointType != "" {
   165  			pointType = config.Destination.PointType
   166  		}
   167  		pointKey := "0"
   168  		if config.Destination.PointKey != "" {
   169  			pointKey = config.Destination.PointKey
   170  		}
   171  		lastBatchTime := time.Now()
   172  		t := time.NewTicker(time.Hour)
   173  		t.Stop()
   174  
   175  		// generateBatch generates a batch of points for the time interval
   176  		// [start, stop) based on the signal generator parameters.
   177  		var generateBatch func(start, stop time.Time) (data.Points, time.Time)
   178  
   179  		if configValid {
   180  			if config.SignalType == "random walk" {
   181  				sampleInterval := time.Duration(
   182  					float64(time.Second) / config.SampleRate,
   183  				)
   184  				generateBatch = func(start, stop time.Time) (data.Points, time.Time) {
   185  					numPoints := int(
   186  						stop.Sub(start).Seconds() * config.SampleRate,
   187  					)
   188  					endTime := start.Add(time.Duration(numPoints) * sampleInterval)
   189  					if numPoints > BatchSizeLimit {
   190  						numPoints = BatchSizeLimit
   191  					}
   192  					pts := make(data.Points, numPoints)
   193  					for i := 0; i < numPoints; i++ {
   194  						val := lastValue + config.MinIncrement + rand.Float64()*
   195  							(config.MaxIncrement-config.MinIncrement)
   196  						pts[i] = data.Point{
   197  							Type: pointType,
   198  							Time: start.Add(time.Duration(i) * sampleInterval),
   199  							Key:  pointKey,
   200  							Value: clamp(
   201  								round(val, config.RoundTo),
   202  								config.MinValue,
   203  								config.MaxValue,
   204  							),
   205  							Origin: config.ID,
   206  						}
   207  						lastValue = clamp(val, config.MinValue, config.MaxValue)
   208  					}
   209  					return pts, endTime
   210  				}
   211  			} else {
   212  				// waveFunc converts radians into a scaled wave output
   213  				var waveFunc func(float64) float64
   214  				switch config.SignalType {
   215  				case "sine":
   216  					waveFunc = func(x float64) float64 {
   217  						return (math.Sin(x)+1)/2*amplitude + config.MinValue
   218  					}
   219  				case "square":
   220  					waveFunc = func(x float64) float64 {
   221  						if x >= math.Pi {
   222  							return config.MaxValue
   223  						}
   224  						return config.MinValue
   225  					}
   226  				case "triangle":
   227  					// https://stackoverflow.com/a/22400799/360539
   228  					waveFunc = func(x float64) float64 {
   229  						const p = math.Pi // p is the half-period
   230  						return (amplitude/p)*
   231  							(p-math.Abs(math.Mod(x, (2*p))-p)) +
   232  							config.MinValue
   233  					}
   234  				}
   235  
   236  				// dx is the change in x per point
   237  				// Taking SampleRate samples should give Frequency cycles
   238  				dx := 2 * math.Pi * config.Frequency / config.SampleRate
   239  				sampleInterval := time.Duration(
   240  					float64(time.Second) / config.SampleRate,
   241  				)
   242  				generateBatch = func(start, stop time.Time) (data.Points, time.Time) {
   243  					numPoints := int(
   244  						stop.Sub(start).Seconds() * config.SampleRate,
   245  					)
   246  					endTime := start.Add(time.Duration(numPoints) * sampleInterval)
   247  					if numPoints > BatchSizeLimit {
   248  						numPoints = BatchSizeLimit
   249  					}
   250  					pts := make(data.Points, numPoints)
   251  					for i := 0; i < numPoints; i++ {
   252  						// Note: lastValue is in terms of x (i.e. time)
   253  						lastValue += dx
   254  						if lastValue >= 2*math.Pi {
   255  							// Prevent lastValue from growing large
   256  							lastValue -= 2 * math.Pi
   257  						}
   258  						y := waveFunc(lastValue)
   259  						y = clamp(
   260  							round(y, config.RoundTo),
   261  							config.MinValue,
   262  							config.MaxValue,
   263  						)
   264  						pts[i] = data.Point{
   265  							Type:   pointType,
   266  							Time:   start.Add(time.Duration(i) * sampleInterval),
   267  							Key:    pointKey,
   268  							Value:  y,
   269  							Origin: config.ID,
   270  						}
   271  					}
   272  					return pts, endTime
   273  				}
   274  			}
   275  
   276  			// Start batch timer
   277  			batchD := time.Duration(config.BatchPeriod) * time.Millisecond
   278  			sampleD := time.Duration(float64(time.Second) / config.SampleRate)
   279  			if batchD > 0 && batchD > sampleD {
   280  				t.Reset(batchD)
   281  			} else {
   282  				t.Reset(sampleD)
   283  			}
   284  		}
   285  
   286  		for {
   287  			select {
   288  			case stopTime := <-t.C:
   289  				pts, endTime := generateBatch(lastBatchTime, stopTime)
   290  				// Send points
   291  				if pts.Len() > 0 {
   292  					lastBatchTime = endTime
   293  					err := SendPoints(sgc.nc, natsSubject, pts, false)
   294  					if err != nil {
   295  						sgc.log.Printf("Error sending points: %v", err)
   296  					}
   297  				}
   298  			case <-chStopGen:
   299  				return
   300  			}
   301  		}
   302  	}
   303  
   304  	go generator(sgc.config)
   305  
   306  done:
   307  	for {
   308  		select {
   309  		case <-sgc.stop:
   310  			chStopGen <- struct{}{}
   311  			sgc.log.Printf("Stopped client: %v", sgc.config.Description)
   312  			break done
   313  		case pts := <-sgc.newPoints:
   314  			err := data.MergePoints(pts.ID, pts.Points, &sgc.config)
   315  			if err != nil {
   316  				sgc.log.Printf("Error merging new points: %v", err)
   317  			}
   318  
   319  			for _, p := range pts.Points {
   320  				switch p.Type {
   321  				case data.PointTypeDisabled,
   322  					data.PointTypeSignalType,
   323  					data.PointTypeMinValue,
   324  					data.PointTypeMaxValue,
   325  					data.PointTypeInitialValue,
   326  					data.PointTypeRoundTo,
   327  					data.PointTypeSampleRate,
   328  					data.PointTypeDestination,
   329  					data.PointTypeBatchPeriod,
   330  					data.PointTypeFrequency,
   331  					data.PointTypeMinIncrement,
   332  					data.PointTypeMaxIncrement:
   333  					// restart generator
   334  					chStopGen <- struct{}{}
   335  					go generator(sgc.config)
   336  				}
   337  			}
   338  
   339  		case pts := <-sgc.newEdgePoints:
   340  			err := data.MergeEdgePoints(pts.ID, pts.Parent, pts.Points, &sgc.config)
   341  			if err != nil {
   342  				sgc.log.Printf("Error merging new points: %v", err)
   343  			}
   344  		}
   345  	}
   346  
   347  	// clean up
   348  	return nil
   349  }
   350  
   351  // Stop sends a signal to the Run function to exit
   352  func (sgc *SignalGeneratorClient) Stop(_ error) {
   353  	close(sgc.stop)
   354  }
   355  
   356  // Points is called by the Manager when new points for this
   357  // node are received.
   358  func (sgc *SignalGeneratorClient) Points(nodeID string, points []data.Point) {
   359  	sgc.newPoints <- NewPoints{nodeID, "", points}
   360  }
   361  
   362  // EdgePoints is called by the Manager when new edge points for this
   363  // node are received.
   364  func (sgc *SignalGeneratorClient) EdgePoints(nodeID, parentID string, points []data.Point) {
   365  	sgc.newEdgePoints <- NewPoints{nodeID, parentID, points}
   366  }