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 }