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  }