gobot.io/x/gobot@v1.16.0/platforms/particle/adaptor.go (about)

     1  package particle
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"net/url"
    10  	"strconv"
    11  
    12  	"github.com/donovanhide/eventsource"
    13  	"gobot.io/x/gobot"
    14  )
    15  
    16  // Adaptor is the Gobot Adaptor for Particle
    17  type Adaptor struct {
    18  	name        string
    19  	DeviceID    string
    20  	AccessToken string
    21  	APIServer   string
    22  	servoPins   map[string]bool
    23  	gobot.Eventer
    24  }
    25  
    26  // Event is an event emitted by the Particle cloud
    27  type Event struct {
    28  	Name  string
    29  	Data  string
    30  	Error error
    31  }
    32  
    33  var eventSource = func(url string) (chan eventsource.Event, chan error, error) {
    34  	stream, err := eventsource.Subscribe(url, "")
    35  	if err != nil {
    36  		return nil, nil, err
    37  	}
    38  	return stream.Events, stream.Errors, nil
    39  }
    40  
    41  // NewAdaptor creates new Photon adaptor with deviceId and accessToken
    42  // using api.particle.io server as default
    43  func NewAdaptor(deviceID string, accessToken string) *Adaptor {
    44  	return &Adaptor{
    45  		name:        gobot.DefaultName("Particle"),
    46  		DeviceID:    deviceID,
    47  		AccessToken: accessToken,
    48  		servoPins:   make(map[string]bool),
    49  		APIServer:   "https://api.particle.io",
    50  		Eventer:     gobot.NewEventer(),
    51  	}
    52  }
    53  
    54  // Name returns the Adaptor name
    55  func (s *Adaptor) Name() string { return s.name }
    56  
    57  // SetName sets the Adaptor name
    58  func (s *Adaptor) SetName(n string) { s.name = n }
    59  
    60  // Connect returns true if connection to Particle Photon or Electron is successful
    61  func (s *Adaptor) Connect() (err error) {
    62  	return
    63  }
    64  
    65  // Finalize returns true if connection to Particle Photon or Electron is finalized successfully
    66  func (s *Adaptor) Finalize() (err error) {
    67  	return
    68  }
    69  
    70  // AnalogRead reads analog ping value using Particle cloud api
    71  func (s *Adaptor) AnalogRead(pin string) (val int, err error) {
    72  	params := url.Values{
    73  		"params":       {pin},
    74  		"access_token": {s.AccessToken},
    75  	}
    76  
    77  	url := fmt.Sprintf("%v/analogread", s.deviceURL())
    78  
    79  	resp, err := s.request("POST", url, params)
    80  	if err == nil {
    81  		val = int(resp["return_value"].(float64))
    82  		return
    83  	}
    84  
    85  	return 0, err
    86  }
    87  
    88  // PwmWrite writes in pin using analog write api
    89  func (s *Adaptor) PwmWrite(pin string, level byte) (err error) {
    90  	return s.AnalogWrite(pin, level)
    91  }
    92  
    93  // AnalogWrite writes analog pin with specified level using Particle cloud api
    94  func (s *Adaptor) AnalogWrite(pin string, level byte) (err error) {
    95  	params := url.Values{
    96  		"params":       {fmt.Sprintf("%v,%v", pin, level)},
    97  		"access_token": {s.AccessToken},
    98  	}
    99  	url := fmt.Sprintf("%v/analogwrite", s.deviceURL())
   100  	_, err = s.request("POST", url, params)
   101  	return
   102  }
   103  
   104  // DigitalWrite writes to a digital pin using Particle cloud api
   105  func (s *Adaptor) DigitalWrite(pin string, level byte) (err error) {
   106  	params := url.Values{
   107  		"params":       {fmt.Sprintf("%v,%v", pin, s.pinLevel(level))},
   108  		"access_token": {s.AccessToken},
   109  	}
   110  	url := fmt.Sprintf("%v/digitalwrite", s.deviceURL())
   111  	_, err = s.request("POST", url, params)
   112  	return err
   113  }
   114  
   115  // DigitalRead reads from digital pin using Particle cloud api
   116  func (s *Adaptor) DigitalRead(pin string) (val int, err error) {
   117  	params := url.Values{
   118  		"params":       {pin},
   119  		"access_token": {s.AccessToken},
   120  	}
   121  	url := fmt.Sprintf("%v/digitalread", s.deviceURL())
   122  	resp, err := s.request("POST", url, params)
   123  	if err == nil {
   124  		val = int(resp["return_value"].(float64))
   125  		return
   126  	}
   127  	return -1, err
   128  }
   129  
   130  // ServoWrite writes the 0-180 degree angle to the specified pin.
   131  // To use it requires installing the "tinker-servo" sketch on your
   132  // Particle device. not just the default "tinker".
   133  func (s *Adaptor) ServoWrite(pin string, angle byte) (err error) {
   134  	if _, present := s.servoPins[pin]; !present {
   135  		err = s.servoPinOpen(pin)
   136  		if err != nil {
   137  			return
   138  		}
   139  	}
   140  
   141  	params := url.Values{
   142  		"params":       {fmt.Sprintf("%v,%v", pin, angle)},
   143  		"access_token": {s.AccessToken},
   144  	}
   145  	url := fmt.Sprintf("%v/servoSet", s.deviceURL())
   146  	_, err = s.request("POST", url, params)
   147  	return err
   148  }
   149  
   150  // EventStream returns a gobot.Event based on the following params:
   151  //
   152  // * source - "all"/"devices"/"device" (More info at: http://docs.particle.io/api/#reading-data-from-a-core-events)
   153  // * name  - Event name to subscribe for, leave blank to subscribe to all events.
   154  //
   155  // A new event is emitted as a particle.Event struct
   156  func (s *Adaptor) EventStream(source string, name string) (event *gobot.Event, err error) {
   157  	var url string
   158  
   159  	switch source {
   160  	case "all":
   161  		url = fmt.Sprintf("%s/v1/events/%s?access_token=%s", s.APIServer, name, s.AccessToken)
   162  	case "devices":
   163  		url = fmt.Sprintf("%s/v1/devices/events/%s?access_token=%s", s.APIServer, name, s.AccessToken)
   164  	case "device":
   165  		url = fmt.Sprintf("%s/events/%s?access_token=%s", s.deviceURL(), name, s.AccessToken)
   166  	default:
   167  		err = errors.New("source param should be: all, devices or device")
   168  		return
   169  	}
   170  
   171  	events, _, err := eventSource(url)
   172  	if err != nil {
   173  		return
   174  	}
   175  
   176  	go func() {
   177  		for {
   178  			select {
   179  			case ev := <-events:
   180  				if ev.Event() != "" && ev.Data() != "" {
   181  					s.Publish(ev.Event(), ev.Data())
   182  				}
   183  			}
   184  		}
   185  	}()
   186  	return
   187  }
   188  
   189  // Variable returns a core variable value as a string
   190  func (s *Adaptor) Variable(name string) (result string, err error) {
   191  	url := fmt.Sprintf("%v/%s?access_token=%s", s.deviceURL(), name, s.AccessToken)
   192  	resp, err := s.request("GET", url, nil)
   193  
   194  	if err != nil {
   195  		return
   196  	}
   197  
   198  	val := resp["result"]
   199  	switch val.(type) {
   200  	case bool:
   201  		result = strconv.FormatBool(val.(bool))
   202  	case float64:
   203  		result = strconv.FormatFloat(val.(float64), 'f', -1, 64)
   204  	case string:
   205  		result = val.(string)
   206  	}
   207  
   208  	return
   209  }
   210  
   211  // Function executes a core function and
   212  // returns value from request.
   213  // Takes a String as the only argument and returns an Int.
   214  // If function is not defined in core, it will time out
   215  func (s *Adaptor) Function(name string, args string) (val int, err error) {
   216  	params := url.Values{
   217  		"args":         {args},
   218  		"access_token": {s.AccessToken},
   219  	}
   220  
   221  	url := fmt.Sprintf("%s/%s", s.deviceURL(), name)
   222  	resp, err := s.request("POST", url, params)
   223  
   224  	if err != nil {
   225  		return -1, err
   226  	}
   227  
   228  	val = int(resp["return_value"].(float64))
   229  	return
   230  }
   231  
   232  // setAPIServer sets Particle cloud api server, this can be used to change from default api.spark.io
   233  func (s *Adaptor) setAPIServer(server string) {
   234  	s.APIServer = server
   235  }
   236  
   237  // deviceURL constructs device url to make requests from Particle cloud api
   238  func (s *Adaptor) deviceURL() string {
   239  	if len(s.APIServer) <= 0 {
   240  		s.setAPIServer("https://api.particle.io")
   241  	}
   242  	return fmt.Sprintf("%v/v1/devices/%v", s.APIServer, s.DeviceID)
   243  }
   244  
   245  // pinLevel converts byte level to string expected in api
   246  func (s *Adaptor) pinLevel(level byte) string {
   247  	if level == 1 {
   248  		return "HIGH"
   249  	}
   250  	return "LOW"
   251  }
   252  
   253  // request makes request to Particle cloud server, return err != nil if there is
   254  // any issue with the request.
   255  func (s *Adaptor) request(method string, url string, params url.Values) (m map[string]interface{}, err error) {
   256  	var resp *http.Response
   257  
   258  	if method == "POST" {
   259  		resp, err = http.PostForm(url, params)
   260  	} else if method == "GET" {
   261  		resp, err = http.Get(url)
   262  	}
   263  
   264  	if err != nil {
   265  		return
   266  	}
   267  
   268  	buf, err := ioutil.ReadAll(resp.Body)
   269  
   270  	if err != nil {
   271  		return
   272  	}
   273  
   274  	json.Unmarshal(buf, &m)
   275  
   276  	if resp.Status != "200 OK" {
   277  		err = fmt.Errorf("%v: error communicating to the Particle cloud", resp.Status)
   278  	} else if _, ok := m["error"]; ok {
   279  		err = errors.New(m["error"].(string))
   280  	}
   281  
   282  	return
   283  }
   284  
   285  func (s *Adaptor) servoPinOpen(pin string) error {
   286  	params := url.Values{
   287  		"params":       {fmt.Sprintf("%v", pin)},
   288  		"access_token": {s.AccessToken},
   289  	}
   290  	url := fmt.Sprintf("%v/servoOpen", s.deviceURL())
   291  	_, err := s.request("POST", url, params)
   292  	if err != nil {
   293  		return err
   294  	}
   295  	s.servoPins[pin] = true
   296  	return nil
   297  }