package main import ( "errors" "strings" "sync" "time" ) // Handoff lets a phone fill in settings the car would otherwise need its on-screen keyboard // for: the ABRP credentials above all, which are long. The car opens a request and shows it as // a QR code, the phone fills in a form (with the approval password), and the car, polling, // collects the values once. // // The values only pass through: they are held in memory until the car collects them, or for // the request's ten minutes, and are never stored, logged or echoed back to the page. type Handoff struct { Now func() time.Time mu sync.Mutex byDevice map[string]*handoffRequest byUser map[string]*handoffRequest } // maxPendingHandoffs bounds memory; only an authenticated car can open one. const maxPendingHandoffs = 200 // PhoneSettings is what the phone may set. A nil or empty field leaves the car's value alone. type PhoneSettings struct { AbrpAPIKey string `json:"abrpApiKey,omitempty"` AbrpToken string `json:"abrpToken,omitempty"` PricePerKwh *float64 `json:"pricePerKwh,omitempty"` UsableKwh *float64 `json:"usableKwh,omitempty"` } func (s PhoneSettings) empty() bool { return s.AbrpAPIKey == "" && s.AbrpToken == "" && s.PricePerKwh == nil && s.UsableKwh == nil } type handoffRequest struct { deviceCode string userCode string installationID string expires time.Time settings *PhoneSettings // set by the phone, handed to the car once } var errAlreadyFilled = errors.New("these settings were already sent") func NewHandoff() *Handoff { return &Handoff{Now: time.Now, byDevice: map[string]*handoffRequest{}, byUser: map[string]*handoffRequest{}} } func (h *Handoff) expireLocked(now time.Time) { for code, r := range h.byDevice { if now.After(r.expires) { delete(h.byDevice, code) delete(h.byUser, r.userCode) } } } // Start opens a request for a car; a new one replaces the same installation's open request. func (h *Handoff) Start(installationID string) (*handoffRequest, error) { h.mu.Lock() defer h.mu.Unlock() now := h.Now() h.expireLocked(now) for _, r := range h.byDevice { if strings.EqualFold(r.installationID, installationID) { delete(h.byDevice, r.deviceCode) delete(h.byUser, r.userCode) } } if len(h.byDevice) >= maxPendingHandoffs { return nil, errTooMany } r := &handoffRequest{deviceCode: randomString(32), installationID: installationID, expires: now.Add(pairingTTL)} for { r.userCode = newUserCode() if _, taken := h.byUser[r.userCode]; !taken { break } } h.byDevice[r.deviceCode] = r h.byUser[r.userCode] = r out := *r return &out, nil } // Open reports whether a user code belongs to a request still waiting for the phone. func (h *Handoff) Open(userCode string) bool { h.mu.Lock() defer h.mu.Unlock() h.expireLocked(h.Now()) r, ok := h.byUser[normalizeUserCode(userCode)] return ok && r.settings == nil } // Fill stores what the phone entered, once. func (h *Handoff) Fill(userCode string, s PhoneSettings) error { h.mu.Lock() defer h.mu.Unlock() h.expireLocked(h.Now()) r, ok := h.byUser[normalizeUserCode(userCode)] if !ok { return errUnknownCode } if r.settings != nil { return errAlreadyFilled } r.settings = &s return nil } // Poll is the car's side: nil while the phone has not sent anything, the settings exactly // once, then errUnknownCode. installationID must be the one the request was opened for. func (h *Handoff) Poll(deviceCode string, mayUse func(installationID string) bool) (*PhoneSettings, error) { h.mu.Lock() defer h.mu.Unlock() h.expireLocked(h.Now()) r, ok := h.byDevice[deviceCode] if !ok || !mayUse(r.installationID) { return nil, errUnknownCode } if r.settings == nil { return nil, nil } delete(h.byDevice, r.deviceCode) delete(h.byUser, r.userCode) return r.settings, nil }