github.com/simpleiot/simpleiot@v0.18.3/client/particle.go (about) 1 package client 2 3 import ( 4 "encoding/json" 5 "log" 6 "time" 7 8 "github.com/donovanhide/eventsource" 9 "github.com/nats-io/nats.go" 10 "github.com/simpleiot/simpleiot/data" 11 ) 12 13 // Get Particle.io data using their event API. See: 14 // https://docs.particle.io/reference/cloud-apis/api/#get-a-stream-of-events 15 16 const particleEventURL string = "https://api.particle.io/v1/devices/events/" 17 18 // ParticleEvent from particle 19 type ParticleEvent struct { 20 Data string `json:"data"` 21 TTL uint32 `json:"ttl"` 22 Timestamp time.Time `json:"published_at"` 23 CoreID string `json:"coreid"` 24 } 25 26 // Particle represents the configuration for the SIOT Particle client 27 type Particle struct { 28 ID string `node:"id"` 29 Parent string `node:"parent"` 30 Description string `point:"description"` 31 Disabled bool `point:"disabled"` 32 AuthToken string `point:"authToken"` 33 } 34 35 // ParticleClient is a SIOT particle client 36 type ParticleClient struct { 37 nc *nats.Conn 38 config Particle 39 stop chan struct{} 40 newPoints chan NewPoints 41 newEdgePoints chan NewPoints 42 newParticlePoints chan NewPoints 43 } 44 45 type particlePoint struct { 46 ID string `json:"id"` 47 Type string `json:"type"` 48 Value float64 `json:"value"` 49 } 50 51 func (pp *particlePoint) toPoint() data.Point { 52 return data.Point{ 53 Key: pp.ID, 54 Type: pp.Type, 55 Value: pp.Value, 56 } 57 } 58 59 // NewParticleClient ... 60 func NewParticleClient(nc *nats.Conn, config Particle) Client { 61 return &ParticleClient{ 62 nc: nc, 63 config: config, 64 stop: make(chan struct{}), 65 newPoints: make(chan NewPoints), 66 newEdgePoints: make(chan NewPoints), 67 newParticlePoints: make(chan NewPoints), 68 } 69 } 70 71 // Run runs the main logic for this client and blocks until stopped 72 func (pc *ParticleClient) Run() error { 73 log.Println("Starting particle client:", pc.config.Description) 74 75 closeReader := make(chan struct{}) // is closed to close reader 76 readerClosed := make(chan struct{}) // struct{} is sent when reader exits 77 var readerRunning bool // indicates reader is running 78 79 particleReader := func() { 80 defer func() { 81 readerClosed <- struct{}{} 82 }() 83 84 urlAuth := particleEventURL + "sample" + "?access_token=" + pc.config.AuthToken 85 86 stream, err := eventsource.Subscribe(urlAuth, "") 87 88 if err != nil { 89 log.Println("Particle subscription error:", err) 90 return 91 } 92 93 for { 94 select { 95 case event := <-stream.Events: 96 var pEvent ParticleEvent 97 err := json.Unmarshal([]byte(event.Data()), &pEvent) 98 if err != nil { 99 log.Println("Got error decoding particle event:", err) 100 continue 101 } 102 103 var pPoints []particlePoint 104 err = json.Unmarshal([]byte(pEvent.Data), &pPoints) 105 if err != nil { 106 log.Println("error decoding Particle samples:", err) 107 continue 108 } 109 110 points := make(data.Points, len(pPoints)) 111 112 for i, p := range pPoints { 113 points[i] = p.toPoint() 114 points[i].Time = pEvent.Timestamp 115 } 116 117 err = SendNodePoints(pc.nc, pc.config.ID, points, false) 118 if err != nil { 119 log.Println("Particle error sending points:", err) 120 } 121 122 case err := <-stream.Errors: 123 log.Println("Particle error:", err) 124 125 case <-closeReader: 126 log.Println("Exiting particle reader") 127 return 128 } 129 } 130 } 131 132 checkTime := time.Minute 133 checkReader := time.NewTicker(checkTime) 134 135 startReader := func() { 136 if readerRunning { 137 return 138 } 139 readerRunning = true 140 go particleReader() 141 checkReader.Stop() 142 } 143 144 stopReader := func() { 145 if readerRunning { 146 closeReader <- struct{}{} 147 readerRunning = false 148 } 149 } 150 151 startReader() 152 153 done: 154 for { 155 select { 156 case <-pc.stop: 157 log.Println("Stopping particle client:", pc.config.Description) 158 break done 159 case pts := <-pc.newPoints: 160 err := data.MergePoints(pts.ID, pts.Points, &pc.config) 161 if err != nil { 162 log.Println("error merging new points:", err) 163 } 164 165 for _, p := range pts.Points { 166 switch p.Type { 167 case data.PointTypeAuthToken: 168 stopReader() 169 startReader() 170 case data.PointTypeDisabled: 171 if p.Value == 1 { 172 stopReader() 173 } else { 174 startReader() 175 } 176 } 177 } 178 179 case pts := <-pc.newEdgePoints: 180 err := data.MergeEdgePoints(pts.ID, pts.Parent, pts.Points, &pc.config) 181 if err != nil { 182 log.Println("error merging new points:", err) 183 } 184 185 case <-readerClosed: 186 readerRunning = false 187 checkReader.Reset(checkTime) 188 189 case <-checkReader.C: 190 startReader() 191 } 192 } 193 194 // clean up 195 stopReader() 196 return nil 197 } 198 199 // Stop sends a signal to the Run function to exit 200 func (pc *ParticleClient) Stop(_ error) { 201 close(pc.stop) 202 } 203 204 // Points is called by the Manager when new points for this 205 // node are received. 206 func (pc *ParticleClient) Points(nodeID string, points []data.Point) { 207 pc.newPoints <- NewPoints{nodeID, "", points} 208 } 209 210 // EdgePoints is called by the Manager when new edge points for this 211 // node are received. 212 func (pc *ParticleClient) EdgePoints(nodeID, parentID string, points []data.Point) { 213 pc.newEdgePoints <- NewPoints{nodeID, parentID, points} 214 }