Il Raspberry Pi usa uno strumento chiamato motion, che scatta foto (con la camera Pi) quando viene rilevato un movimento. Le foto vengono poi caricate su Cloud Storage, e una Cloud Function rimane in ascolto degli eventi che si attivano quando viene caricato un nuovo oggetto nel bucket. Quando viene caricata una nuova foto, la funzione cerca di etichettare l'immagine usando Vision API e archivia il risultato in Firestore. Firestore a sua volta genera un evento, che ascolto in una diversa Cloud Function, la quale usa IFTTT per notificarmi quando è stato rilevato un movimento e cosa ha trovato Vision API nell'immagine. Lo scopo di questo articolo è spiegare come fare il parsing degli eventi Firestore, che vengono consegnati alla funzione in un formato piuttosto confuso!
Struttura e Forma dello Struct Event
La Cloud Function che elabora i nuovi eventi (= nuovi caricamenti dal Raspberry Pi, quando viene rilevato un movimento), dopo aver etichettato la foto con Vision API, crea un nuovo oggetto Event e lo archivia nella collection Events in Firestore. Lo struct Event si presenta così:
type Event struct {
URI string `json:"uri" firestore:"uri"`
Created time.Time `json:"created" firestore:"created"`
Labels []Label `json:"labels" firestore:"labels"`
}Lo struct fa anche riferimento a un array di Label. Label è uno struct definito come:
type Label struct {
Description string `json:"description" firestore:"description"`
Score float32 `json:"score" firestore:"score"`
}Questo è il risultato una volta che le informazioni sono state persistite in Firestore:

Creare una Funzione per Ascoltare gli Eventi Firestore
Un'altra funzione, chiamata Notify, ascolta gli eventi da Firestore (e poi notifica l'utente tramite IFTTT), che vengono attivati quando vengono aggiunti nuovi dati al database. Ho usato Terraform per configurare la funzione:
resource "google_cloudfunctions_function" "notify" {
project = data.google_project.this.project_id
region = "europe-west1"
name = "Notify"
description = "Notifies of newly labeled uploads"
service_account_email = google_service_account.functions.email
runtime = "go113"
ingress_settings = "ALLOW_INTERNAL_ONLY"
available_memory_mb = 128
entry_point = "Notify"
source_repository {
url = "https://source.developers.google.com/projects/${data.google_project.this.project_id}/repos/syn/moveable-aliases/master/paths/functions"
}
event_trigger {
event_type = "providers/cloud.firestore/eventTypes/document.create"
resource = "Events/{ids}"
}
environment_variables = {
"IFTTT_WEBHOOK_URL" : var.ifttt_webhook_url
}
}Il blocco event_trigger definisce l'evento che la funzione deve ascoltare. In questo caso, ascolto gli eventi providers/cloud.firestore/eventTypes/document.create nella collection Events.
Come Si Presenta un Evento Firestore "Grezzo"?
Usando fmt.Printf("%+v", event), possiamo vedere che l'oggetto evento Firestore si presenta così:
{OldValue:{CreateTime:0001-01-01 00:00:00 +0000 UTC Fields:{Created:{TimestampValue:0001-01-01 00:00:00 +0000 UTC} File:{MapValue:{Fields:{Bucket:{StringValue:} Name:{StringValue:}}}} Labels:{ArrayValue:{Values:[]}}} Name: UpdateTime:0001-01-01 00:00:00 +0000 UTC} Value:{CreateTime:2021-07-27 09:22:03.654255 +0000 UTC Fields:{Created:{TimestampValue:2021-07-27 09:22:01.4 +0000 UTC} File:{MapValue:{Fields:{Bucket:{StringValue:} Name:{StringValue:}}}} Labels:{ArrayValue:{Values:[{MapValue:{Fields:{Description:{StringValue:cat} Score:{DoubleValue:0.8764283061027527}}}} {MapValue:{Fields:{Description:{StringValue:carnivore} Score:{DoubleValue:0.8687784671783447}}}} {MapValue:{Fields:{Description:{StringValue:asphalt} Score:{DoubleValue:0.8434737920761108}}}} {MapValue:{Fields:{Description:{StringValue:felidae} Score:{DoubleValue:0.8221824765205383}}}} {MapValue:{Fields:{Description:{StringValue:road surface} Score:{DoubleValue:0.807261049747467}}}}]}}} Name:projects/cvln-syn/databases/(default)/documents/Events/tVhYbIZBQypHtHzDUabq UpdateTime:2021-07-27 09:22:03.654255 +0000 UTC} UpdateMask:{FieldPaths:[]}}...il che è estremamente confuso! Mi aspettavo che l'evento assomigliasse esattamente ai dati che avevo precedentemente archiviato nel database, ma per qualche motivo questo è il formato degli eventi Firebase. Fortunatamente, il plugin IntelliJ IDEA "JSON to Go Struct" aiuta a dare un senso a quanto sopra:
type FirestoreUpload struct {
Created struct {
TimestampValue time.Time `json:"timestampValue"`
} `json:"created"`
File struct {
MapValue struct {
Fields struct {
Bucket struct {
StringValue string `json:"stringValue"`
} `json:"bucket"`
Name struct {
StringValue string `json:"stringValue"`
} `json:"name"`
} `json:"fields"`
} `json:"mapValue"`
} `json:"file"`
Labels struct {
ArrayValue struct {
Values []struct {
MapValue struct {
Fields struct {
Description struct {
StringValue string `json:"stringValue"`
} `json:"description"`
Score struct {
DoubleValue float64 `json:"doubleValue"`
} `json:"score"`
} `json:"fields"`
} `json:"mapValue"`
} `json:"values"`
} `json:"arrayValue"`
} `json:"labels"`
}Sebbene ancora confuso, almeno ora posso suddividere lo struct in modo da poter fare riferimento ai tipi correttamente altrove nell'applicazione, ad esempio per iterare sulle label.
Pulire la Struttura dell'Evento
FirestoreUpload può essere suddiviso per avere campi con nome invece di struct anonimi. Questo è utile per poter fare riferimento ai campi e ai tipi corretti altrove nell'applicazione, ad esempio quando si itera sulle label.
package events
import (
"github.com/thoas/go-funk"
"time"
)
//FirestoreEvent is the payload of a Firestore event
type FirestoreEvent struct {
OldValue FirestoreValue `json:"oldValue"`
Value FirestoreValue `json:"value"`
UpdateMask struct {
FieldPaths []string `json:"fieldPaths"`
} `json:"updateMask"`
}
// FirestoreValue holds Firestore fields
type FirestoreValue struct {
CreateTime time.Time `json:"createTime"`
Fields FirestoreUpload `json:"fields"`
Name string `json:"name"`
UpdateTime time.Time `json:"updateTime"`
}
// FirestoreUpload represents a Firebase event of a new record in the Upload collection
type FirestoreUpload struct {
Created Created `json:"created"`
File File `json:"file"`
Labels Labels `json:"labels"`
}
type Created struct {
TimestampValue time.Time `json:"timestampValue"`
}
type File struct {
MapValue FileMapValue `json:"mapValue"`
}
type FileMapValue struct {
Fields FileFields `json:"fields"`
}
type FileFields struct {
Bucket StringValue `json:"bucket"`
Name StringValue `json:"name"`
}
type Labels struct {
ArrayValue LabelArrayValue `json:"arrayValue"`
}
type LabelArrayValue struct {
Values []LabelValues `json:"values"`
}
type LabelValues struct {
MapValue LabelsMapValue `json:"mapValue"`
}
type LabelsMapValue struct {
Fields LabelFields `json:"fields"`
}
type LabelFields struct {
Description StringValue `json:"description"`
Score DoubleValue `json:"score"`
}
type StringValue struct {
StringValue string `json:"stringValue"`
}
type DoubleValue struct {
DoubleValue float64 `json:"doubleValue"`
}
// GetUploadLabels returns the labels of the image as an array of strings
func (e FirestoreEvent) GetUploadLabels() []string {
return funk.Map(e.Value.Fields.Labels.ArrayValue.Values, func(l LabelValues) string {
return l.MapValue.Fields.Description.StringValue
}).([]string)
}La funzione GetUploadLabels() è un esempio di come accedere all'oggetto evento FirestoreUpload. Qui sto anche usando il pacchetto go-funk, che aggiunge alcune funzionalità aggiuntive in stile funzionale a Go (ma le prestazioni non sono buone quanto un ciclo "nativo").
Riepilogo
In questo articolo ho spiegato come leggere gli eventi Firestore dalle Cloud Function che li ascoltano. Gli esempi sono scritti in Golang, ma linguaggi diversi dovranno fare il parsing dei messaggi in modo simile. Sebbene non sia comodo, questo è il formato attuale degli eventi Firestore! Fortunatamente, una volta che sai come leggerli, il resto è semplice!
lucavallin

