Kevin Le Brun
Published © MIT

IOTA-Enabled Phone Booth

This is an IOTA-enabled phone booth that you can book in exchange of some tokens.

IntermediateFull instructions provided10 hours857

Things used in this project

Hardware components

ESP8266 ESP-12E
Espressif ESP8266 ESP-12E
×1
Waveshare 2.7 3 colors e-ink display
×1

Software apps and online services

Arduino IDE
Arduino IDE
IOTA Tangle
IOTA Tangle

Story

Read more

Schematics

Project code, stl, and schematics

Where you can see updated files.

Code

main.go

Go
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"os/signal"
	"strconv"
	"sync"
	"time"

	mqtt "github.com/eclipse/paho.mqtt.golang"
	iota "github.com/iotaledger/iota.go/api"
	"github.com/iotaledger/iota.go/consts"
	"github.com/iotaledger/iota.go/converter"
	"github.com/iotaledger/iota.go/transaction"
	"github.com/iotaledger/iota.go/trinary"
)

const calendarID = "xxx@group.calendar.google.com"

const IOTAEndpoint = "https://nodes.devnet.iota.org:443"

func main() {
	calendar, err := NewCalendarService()
	if err != nil {
		log.Fatalf("unable to create a calendar service: %v", err)
	}

	var wg sync.WaitGroup
	done := make(chan struct{})

	wg.Add(1)
	go func() {
		defer wg.Done()
		ticker := time.NewTicker(10 * time.Second)
		defer ticker.Stop()
		for {
			select {
				case <-ticker.C:
					publishCalendarState(calendar)
				case <-done:
					return
			}
		}
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		ticker := time.NewTicker(5 * time.Second)
		defer ticker.Stop()

		// Create a new instance of the IOTA API object
		client, err := iota.ComposeAPI(iota.HTTPClientSettings{URI: IOTAEndpoint})
		if err != nil {
			log.Fatalf("unable to create IOTA API: %v", err)
		}

		// Call the `getNodeInfo()` method for information about the node and the Tangle
		nodeInfo, err := client.GetNodeInfo()
		if err != nil {
			log.Fatalf("unable to get node info: %v", err)
		}
		log.Println(nodeInfo)

		for {
			select {
			case <-ticker.C:
				lastTimestamp, err := ioutil.ReadFile("last_transaction_ts")
				if err != nil {
					log.Fatalf("cannot get last transaction timestamp: %v", err)
				}
				lt, err := strconv.ParseUint(string(lastTimestamp), 10, 64)
				if err != nil {
					log.Fatalf("cannot parse last transaction timestamp: %v", err)
				}
				hs, err := client.FindTransactions(iota.FindTransactionsQuery{
					Addresses: trinary.Hashes{"xxx"}, // receiver address
				})
				if err != nil {
					log.Fatalf("cannot get transactions for given address: %v", err)
				}
				for _, h := range hs {
					trytes, err := client.GetTrytes(h)
					if err != nil {
						log.Fatalf("cannot get trytes: %v", err)
					}

					tx := make(transaction.Transactions, 0)
					for _, singleTrytes := range trytes {
						t, err := transaction.AsTransactionObject(singleTrytes)
						if err != nil {
							log.Fatalf("cannot parse transaction: %v", err)
						}
						if t.Timestamp > lt {
							tx = append(tx, *t)
						}
					}

					var lastSeenTimestamp uint64

					for _, t := range tx {
						fmt.Println("------------")
						fmt.Println(t.Value)
						fmt.Println(t.Timestamp)
						if t.Timestamp > lastSeenTimestamp {
							lastSeenTimestamp = t.Timestamp
						}
						bs, _ := converter.TrytesToASCII(t.SignatureMessageFragment[:consts.SignatureMessageFragmentSizeInTrytes-3])
						if err != nil {
							log.Fatalf("failed to decode message: %v", err)
						}
						fmt.Println(bs)

						err = calendar.EnqueueEvent(calendarID, t.Value, bs)
						if err != nil {
							log.Fatalf("failed to create event: %v", err)
						}
					}

					if lastSeenTimestamp > 0 {
						err = ioutil.WriteFile("last_transaction_ts", []byte(fmt.Sprintf("%d", lastSeenTimestamp)), 0600)
						if err != nil {
							log.Fatalf("failed write last seen transaction: %v", err)
						}
					}
				}
			case <-done:
				return
			}
		}
	}()

	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan, os.Interrupt)
	go func() {
		<-signalChan
		close(done)
	}()

	wg.Wait()
}

func publishCalendarState(calendar *CalendarService) {
	next, err := calendar.FindNextFreeSlot(calendarID)
	if err != nil {
		log.Printf("error trying to find next free slot: %v", err)
	}
	sendMQTTMSG(next.Format(time.RFC3339))
}

func sendMQTTMSG(msg string) {
	opts := mqtt.NewClientOptions()
	opts.AddBroker("localhost:1883")
	opts.SetClientID("phonebooth_controller")

  // FIXME extract this logic in a dedicated service
	client := mqtt.NewClient(opts)
	if token := client.Connect(); token.Wait() && token.Error() != nil {
		panic(token.Error())
	}
	token := client.Publish("phonebooth:update", byte(0), false, msg)
	token.Wait()
	client.Disconnect(250)
}

calendar.go

Go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"time"

	"github.com/pkg/errors"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/calendar/v3"
)

const minimumSlotWindow = 30 * time.Minute
const minimumSlotWindowAmount = 30

type CalendarService struct {
	srv *calendar.Service
}

func (c *CalendarService) FindNextFreeSlot(calendarID string) (time.Time, error) {
	t := time.Now().Format(time.RFC3339)
	events, err := c.srv.Events.List(calendarID).ShowDeleted(false).
		SingleEvents(true).TimeMin(t).MaxResults(2500).OrderBy("startTime").Do()
	if err != nil {
		return time.Now(), errors.Wrap(err, "unable to retrieve next ten of the user's events")
	}

	if len(events.Items) == 0 {
		nextFreeSlot := time.Now().Truncate(minimumSlotWindow)
		return nextFreeSlot, nil
	}

	last := events.Items[len(events.Items)-1]
	et, err := time.Parse(time.RFC3339, last.End.DateTime)
	if err != nil {
		return time.Now(), errors.Wrap(err, "failed to parse last event end date")
	}

	nextFreeSlot := et.Truncate(minimumSlotWindow)
	if nextFreeSlot.Before(et) {
		nextFreeSlot = nextFreeSlot.Add(minimumSlotWindow)
	}
	return nextFreeSlot, nil
}

func (c *CalendarService) EnqueueEvent(calendarID string, value int64, label string) error {
	nextStart, err := c.FindNextFreeSlot(calendarID)
	if err != nil {
		return errors.Wrap(err, "cannot find the next free slot")
	}

	value = value - (value % minimumSlotWindowAmount)
	nextEnd := nextStart.Add(time.Duration(value) * time.Minute)

	if value == 0 {
		return nil
	}

	newEvent := calendar.Event{
		Summary: label,
		Start: &calendar.EventDateTime{DateTime: nextStart.Format(time.RFC3339)},
		End: &calendar.EventDateTime{DateTime: nextEnd.Format(time.RFC3339)},
	}

	_, err = c.srv.Events.Insert(calendarID, &newEvent).Do()
	return err
}

func NewCalendarService() (*CalendarService, error) {
	b, err := ioutil.ReadFile("credentials.json")
	if err != nil {
		return nil, errors.Wrap(err, "unable to read client secret file")
	}

	// If modifying these scopes, delete your previously saved token.json.
	config, err := google.ConfigFromJSON(b, calendar.CalendarScope)
	if err != nil {
		return nil, errors.Wrap(err, "unable to parse client secret file to config")
	}

	client, err := getClient(config)
	if err != nil {
		return nil, errors.Wrap(err, "unable to create client")
	}

	srv, err := calendar.New(client)
	if err != nil {
		return nil, errors.Wrap(err, "unable to retrieve Calendar client")
	}

	return &CalendarService{
		srv: srv,
	}, nil
}

// Retrieve a token, saves the token, then returns the generated client.
func getClient(config *oauth2.Config) (*http.Client, error) {
	// The file token.json stores the user's access and refresh tokens, and is
	// created automatically when the authorization flow completes for the first
	// time.
	tokFile := "token.json"
	tok, err := tokenFromFile(tokFile)
	if err != nil {
		tok, err = getTokenFromWeb(config)
		if err != nil {
			return nil, errors.Wrap(err, "unable to get token from WEB")
		}
		err = saveToken(tokFile, tok)
		if err != nil {
			return nil, errors.Wrap(err, "unable to save token to file")
		}
	}
	return config.Client(context.Background(), tok), nil
}

// Request a token from the web, then returns the retrieved token.
func getTokenFromWeb(config *oauth2.Config) (*oauth2.Token, error) {
	authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
	fmt.Printf("Go to the following link in your browser then type the "+
		"authorization code: \n%v\n", authURL)

	var authCode string
	if _, err := fmt.Scan(&authCode); err != nil {
		return nil, errors.Wrap(err, "unable to read authorization code")
	}

	tok, err := config.Exchange(context.TODO(), authCode)
	if err != nil {
		return nil, errors.Wrap(err, "unable to retrieve token from web")
	}
	return tok, nil
}

// Retrieves a token from a local file.
func tokenFromFile(file string) (*oauth2.Token, error) {
	f, err := os.Open(file)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	tok := &oauth2.Token{}
	err = json.NewDecoder(f).Decode(tok)
	return tok, err
}

// Saves a token to a file path.
func saveToken(path string, token *oauth2.Token) error {
	fmt.Printf("Saving credential file to: %s\n", path)
	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		return errors.Wrap(err, "unable to cache oauth token")
	}
	defer f.Close()
	json.NewEncoder(f).Encode(token)
	return nil
}

Credits

Kevin Le Brun

Kevin Le Brun

2 projects • 1 follower

Comments