298 lines
7.8 KiB
Go
298 lines
7.8 KiB
Go
package particle
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
|
|
"github.com/donovanhide/eventsource"
|
|
|
|
"gobot.io/x/gobot/v2"
|
|
)
|
|
|
|
// Adaptor is the Gobot Adaptor for Particle
|
|
type Adaptor struct {
|
|
name string
|
|
DeviceID string
|
|
AccessToken string
|
|
APIServer string
|
|
servoPins map[string]bool
|
|
gobot.Eventer
|
|
}
|
|
|
|
// Event is an event emitted by the Particle cloud
|
|
type Event struct {
|
|
Name string
|
|
Data string
|
|
Error error
|
|
}
|
|
|
|
var eventSource = func(url string) (chan eventsource.Event, chan error, error) {
|
|
stream, err := eventsource.Subscribe(url, "")
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return stream.Events, stream.Errors, nil
|
|
}
|
|
|
|
// NewAdaptor creates new Photon adaptor with deviceId and accessToken
|
|
// using api.particle.io server as default
|
|
func NewAdaptor(deviceID string, accessToken string) *Adaptor {
|
|
return &Adaptor{
|
|
name: gobot.DefaultName("Particle"),
|
|
DeviceID: deviceID,
|
|
AccessToken: accessToken,
|
|
servoPins: make(map[string]bool),
|
|
APIServer: "https://api.particle.io",
|
|
Eventer: gobot.NewEventer(),
|
|
}
|
|
}
|
|
|
|
// Name returns the Adaptor name
|
|
func (s *Adaptor) Name() string { return s.name }
|
|
|
|
// SetName sets the Adaptor name
|
|
func (s *Adaptor) SetName(n string) { s.name = n }
|
|
|
|
// Connect returns true if connection to Particle Photon or Electron is successful
|
|
func (s *Adaptor) Connect() error { return nil }
|
|
|
|
// Finalize returns true if connection to Particle Photon or Electron is finalized successfully
|
|
func (s *Adaptor) Finalize() error { return nil }
|
|
|
|
// AnalogRead reads analog ping value using Particle cloud api
|
|
func (s *Adaptor) AnalogRead(pin string) (int, error) {
|
|
params := url.Values{
|
|
"params": {pin},
|
|
"access_token": {s.AccessToken},
|
|
}
|
|
|
|
url := fmt.Sprintf("%v/analogread", s.deviceURL())
|
|
|
|
resp, err := s.request("POST", url, params)
|
|
if err == nil {
|
|
//nolint:forcetypeassert // ok here
|
|
return int(resp["return_value"].(float64)), nil
|
|
}
|
|
|
|
return 0, err
|
|
}
|
|
|
|
// PwmWrite writes in pin using analog write api
|
|
func (s *Adaptor) PwmWrite(pin string, level byte) error {
|
|
return s.AnalogWrite(pin, level)
|
|
}
|
|
|
|
// AnalogWrite writes analog pin with specified level using Particle cloud api
|
|
func (s *Adaptor) AnalogWrite(pin string, level byte) error {
|
|
params := url.Values{
|
|
"params": {fmt.Sprintf("%v,%v", pin, level)},
|
|
"access_token": {s.AccessToken},
|
|
}
|
|
url := fmt.Sprintf("%v/analogwrite", s.deviceURL())
|
|
_, err := s.request("POST", url, params)
|
|
return err
|
|
}
|
|
|
|
// DigitalWrite writes to a digital pin using Particle cloud api
|
|
func (s *Adaptor) DigitalWrite(pin string, level byte) error {
|
|
params := url.Values{
|
|
"params": {fmt.Sprintf("%v,%v", pin, s.pinLevel(level))},
|
|
"access_token": {s.AccessToken},
|
|
}
|
|
url := fmt.Sprintf("%v/digitalwrite", s.deviceURL())
|
|
_, err := s.request("POST", url, params)
|
|
return err
|
|
}
|
|
|
|
// DigitalRead reads from digital pin using Particle cloud api
|
|
func (s *Adaptor) DigitalRead(pin string) (int, error) {
|
|
params := url.Values{
|
|
"params": {pin},
|
|
"access_token": {s.AccessToken},
|
|
}
|
|
url := fmt.Sprintf("%v/digitalread", s.deviceURL())
|
|
resp, err := s.request("POST", url, params)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
//nolint:forcetypeassert // ok here
|
|
return int(resp["return_value"].(float64)), nil
|
|
}
|
|
|
|
// ServoWrite writes the 0-180 degree angle to the specified pin.
|
|
// To use it requires installing the "tinker-servo" sketch on your
|
|
// Particle device. not just the default "tinker".
|
|
func (s *Adaptor) ServoWrite(pin string, angle byte) error {
|
|
if _, present := s.servoPins[pin]; !present {
|
|
if err := s.servoPinOpen(pin); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
params := url.Values{
|
|
"params": {fmt.Sprintf("%v,%v", pin, angle)},
|
|
"access_token": {s.AccessToken},
|
|
}
|
|
url := fmt.Sprintf("%v/servoSet", s.deviceURL())
|
|
_, err := s.request("POST", url, params)
|
|
return err
|
|
}
|
|
|
|
// EventStream returns a gobot.Event based on the following params:
|
|
//
|
|
// * source - "all"/"devices"/"device" (More info at: http://docs.particle.io/api/#reading-data-from-a-core-events)
|
|
// * name - Event name to subscribe for, leave blank to subscribe to all events.
|
|
//
|
|
// A new event is emitted as a particle.Event struct
|
|
func (s *Adaptor) EventStream(source string, name string) (*gobot.Event, error) {
|
|
var url string
|
|
|
|
switch source {
|
|
case "all":
|
|
url = fmt.Sprintf("%s/v1/events/%s?access_token=%s", s.APIServer, name, s.AccessToken)
|
|
case "devices":
|
|
url = fmt.Sprintf("%s/v1/devices/events/%s?access_token=%s", s.APIServer, name, s.AccessToken)
|
|
case "device":
|
|
url = fmt.Sprintf("%s/events/%s?access_token=%s", s.deviceURL(), name, s.AccessToken)
|
|
default:
|
|
return nil, errors.New("source param should be: all, devices or device")
|
|
}
|
|
|
|
events, _, err := eventSource(url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
ev := <-events
|
|
if ev.Event() != "" && ev.Data() != "" {
|
|
s.Publish(ev.Event(), ev.Data())
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil, nil //nolint:nilnil // seems ok here
|
|
}
|
|
|
|
// Variable returns a core variable value as a string
|
|
func (s *Adaptor) Variable(name string) (string, error) {
|
|
url := fmt.Sprintf("%v/%s?access_token=%s", s.deviceURL(), name, s.AccessToken)
|
|
resp, err := s.request("GET", url, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var result string
|
|
switch val := resp["result"].(type) {
|
|
case bool:
|
|
result = strconv.FormatBool(val)
|
|
case float64:
|
|
result = strconv.FormatFloat(val, 'f', -1, 64)
|
|
case string:
|
|
result = val
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// Function executes a core function and
|
|
// returns value from request.
|
|
// Takes a String as the only argument and returns an Int.
|
|
// If function is not defined in core, it will time out
|
|
func (s *Adaptor) Function(name string, args string) (int, error) {
|
|
params := url.Values{
|
|
"args": {args},
|
|
"access_token": {s.AccessToken},
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/%s", s.deviceURL(), name)
|
|
resp, err := s.request("POST", url, params)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
|
|
//nolint:forcetypeassert // ok here
|
|
return int(resp["return_value"].(float64)), nil
|
|
}
|
|
|
|
// setAPIServer sets Particle cloud api server, this can be used to change from default api.spark.io
|
|
func (s *Adaptor) setAPIServer(server string) {
|
|
s.APIServer = server
|
|
}
|
|
|
|
// deviceURL constructs device url to make requests from Particle cloud api
|
|
func (s *Adaptor) deviceURL() string {
|
|
if len(s.APIServer) == 0 {
|
|
s.setAPIServer("https://api.particle.io")
|
|
}
|
|
return fmt.Sprintf("%v/v1/devices/%v", s.APIServer, s.DeviceID)
|
|
}
|
|
|
|
// pinLevel converts byte level to string expected in api
|
|
func (s *Adaptor) pinLevel(level byte) string {
|
|
if level == 1 {
|
|
return "HIGH"
|
|
}
|
|
return "LOW"
|
|
}
|
|
|
|
// request makes request to Particle cloud server, return err != nil if there is
|
|
// any issue with the request.
|
|
//
|
|
//nolint:bodyclose,noctx // not changed yet
|
|
func (s *Adaptor) request(method string, url string, params url.Values) (map[string]interface{}, error) {
|
|
var resp *http.Response
|
|
var err error
|
|
if method == "POST" {
|
|
resp, err = http.PostForm(url, params) //nolint:gosec // accepted, because local function and no exposed routing
|
|
} else if method == "GET" {
|
|
resp, err = http.Get(url) //nolint:gosec // accepted, because local function and no exposed routing
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
buf, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(buf, &m); err != nil {
|
|
return m, err
|
|
}
|
|
|
|
if resp.Status != "200 OK" {
|
|
return m, fmt.Errorf("%v: error communicating to the Particle cloud", resp.Status)
|
|
}
|
|
|
|
if _, ok := m["error"]; ok {
|
|
//nolint:forcetypeassert // ok here
|
|
return m, errors.New(m["error"].(string))
|
|
}
|
|
|
|
return m, nil
|
|
}
|
|
|
|
func (s *Adaptor) servoPinOpen(pin string) error {
|
|
params := url.Values{
|
|
"params": {pin},
|
|
"access_token": {s.AccessToken},
|
|
}
|
|
url := fmt.Sprintf("%v/servoOpen", s.deviceURL())
|
|
_, err := s.request("POST", url, params)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.servoPins[pin] = true
|
|
return nil
|
|
}
|