30 Jul 2017, 20:58

maslow's hierarchy of needs

After self-actualization, a little known fact is that the next, tiniest part of the pyramid is the ability find your cell phone before you leave. The normal solution is to have someone call it for you- sound being a part of the hierarchy of senses- but if you’re alone, trying to leave, and you can’t find the thing, what then?

I bought this little board for almost nothing, got a trial account at Twilio, and hacked up a little program.

Calling Twilio was most of the work-

type TwilioPlay struct {
	Name xml.Name `xml:"Response"`

	Say  string `xml:",omitempty"`
	Play string `xml:",omitempty"`
}

type TwilioResponse struct {
	Sid         *string      `json:"sid"`
	DateCreated *string      `json:"date_created"`
	DateUpdated *string      `json:"date_updated"`
	DateSent    *interface{} `json:"date_sent"`
	AccountSid  *string      `json:"account_sid"`
	To          *string      `json:"to"`
	From        *string      `json:"from"`
	Body        *string      `json:"body"`
	Status      string       `json:"status"`
	Flags       *[]string    `json:"flags"`
	APIVersion  *string      `json:"api_version"`
	Price       *interface{} `json:"price"`
	URI         *string      `json:"uri"`
	// Optional exception params
	Message  *string `json:"message"`
	MoreInfo *string `json:"more_info"`
	Code     *int    `json:"code"`
}

func Call(toNumber, fromNumber, sid, token, twimlURL string) {
	u := url.URL{
		Scheme: "https",
		Host:   "api.twilio.com",
		Path:   path.Join("2010-04-01/Accounts/", sid, "/Calls.json"),
	}
	q := u.Query()
	q.Set("To", toNumber)
	q.Set("From", fromNumber)
	q.Set("Url", twimlURL)

	r, err := http.NewRequest("POST", u.String(), strings.NewReader(q.Encode()))
	if err != nil {
		panic(err)
	}
	r.SetBasicAuth(sid, token)
	r.Header.Add("Accept", "application/json")
	r.Header.Add("Content-Type", "application/x-www-form-urlencoded")

	resp, err := (&http.Client{}).Do(r)
	if err != nil {
		panic(err)
	}

	var tresp TwilioResponse
	body, err := ioutil.ReadAll(resp.Body)
	err = json.Unmarshal(body, &tresp)
	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		// fine
	} else {
		panic(fmt.Sprintf(*tresp.Message))
	}
}

Possibly not best practice, but my theory here is to just crash the program when things go wrong, and let systemd restart it. So there’s no real error handling anywhere, just unconditional panics.

To trigger the thing, I hooked up a button to the board’s GPIO pin 18. linux-sunxi.org has good documentation, but as someone without much experience with the conventions of little single board computers, it took me a while to figure out how to map their GPIO table to the pins on the board. The trick is there is a little ▶ character next to pin 1- count from there left to right, top to bottom.

The GPIO filesystem is very unixy, and made it really easy to test that the wiring was working. Hold the button down, cat /sys/class/gpio/gpio18/value. Let go of the button, cat /sys/class/gpio/gpio18/value. I spent some time wandering around looking for a Go library, and had some bad luck, so I went ahead and wrote my own. The documentation mentions that

    If the pin can be configured as interrupt-generating interrupt
    and if it has been configured to generate interrupts (see the
    description of "edge"), you can poll(2) on that file and
    poll(2) will return whenever the interrupt was triggered. If
    you use poll(2), set the events POLLPRI and POLLERR. If you
    use select(2), set the file descriptor in exceptfds. After
    poll(2) returns, either lseek(2) to the beginning of the sysfs
    file and read the new value or close the file and re-open it
    to read the value.

So I went ahead and did that-

func poller(pin string, values chan []byte) {
	var fds []unix.PollFd = make([]unix.PollFd, 1)

	fd, err := os.Open(pin)
	fds[0] = unix.PollFd{Fd: int32(fd.Fd()), Events: unix.POLLPRI | unix.POLLERR}
	for {
		if err != nil {
			panic(err)
		} // everything is busted
		n, err := unix.Poll(fds, 10000)
		if err != nil {
			panic(err)
		} // everything is definitely busted
		if n == 1 {
			contents, err := ioutil.ReadAll(fd)
			if err != nil {
				panic(err)
			} else {
				values <- contents
			}
			_, err = fd.Seek(0, 0)
			if err != nil {
				panic(err)
			}
		}
	}
}

Throwing it all together-

func main() {
	var pin string = "/sys/class/gpio/gpio18/value"

	throttle := time.Tick(time.Second * 30)

	values := make(chan []byte)

	go poller(pin, values)

	for {
		edge := <-values
		// we're doing pulldown
		if bytes.Compare(edge, []byte{'0', '\n'}) == 0 {
			Call(call_number, from_number, os.Getenv("SID"), os.Getenv("TOKEN"), twimlURL)
			<-throttle
		}
	}
}

I needed the goroutine and channel for the initial implementation, but since I switched to poll(2), I guess I could just have one loop.

systemd, as always, makes the whole “I would like a supervised process” problem trivial-

[Unit]
Description=Monitor a GPIO port, and call a phone when it goes low

[Service]
Environment=SID='{{sid}}' TOKEN='{{token}}'
# Turn on the GPIO port
ExecStartPre=/bin/echo "18" > /sys/class/gpio/export
# Set high/low behavior
ExecStartPre=/bin/echo both > /sys/class/gpio/gpio18/edge
Exec=/usr/local/bin/findphone
Restart=always

[Install]
WantedBy=multi-user.target

Hopefully this is helpful to someone somehow.