Skip to content
This repository was archived by the owner on Mar 17, 2025. It is now read-only.

Refactored Firebase library #42

Merged
merged 19 commits into from
Jan 29, 2016
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 102 additions & 75 deletions Firebase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,116 +15,143 @@
//
#include "Firebase.h"

const char* firebaseFingerprint = "7A 54 06 9B DC 7A 25 B3 86 8D 66 53 48 2C 0B 96 42 C7 B3 0A";
const uint16_t firebasePort = 443;
namespace {
const char* kFirebaseFingerprint = "7A 54 06 9B DC 7A 25 B3 86 8D 66 53 48 2C 0B 96 42 C7 B3 0A";
const uint16_t kFirebasePort = 443;

Firebase::Firebase(const String& host) : _host(host) {
_http.setReuse(true);
String makeUrl(const String& path, const String& auth) {
String url;
if (path[0] != '/') {
url = "/";
}
url += path + ".json";
if (auth.length() > 0) {
url += "?auth=" + auth;
}
return url;
}

} // namespace

Firebase::Firebase(const String& host) : host_(host) {
http_.setReuse(true);
}

Firebase& Firebase::auth(const String& auth) {
_auth = auth;
auth_ = auth;
return *this;
}

String Firebase::get(const String& path) {
return sendRequestGetBody("GET", path);
FirebaseCall Firebase::get(const String& path) {
return FirebaseCall(host_, auth_, "GET", path, &http_);
}

String Firebase::push(const String& path, const String& value) {
return sendRequestGetBody("POST", path, value);
FirebaseCall Firebase::push(const String& path, const String& value) {
return FirebaseCall(host_, auth_, "POST", path, value, &http_);
}

bool Firebase::remove(const String& path) {
int status = sendRequest("DELETE", path);
return status == HTTP_CODE_OK;
FirebaseCall Firebase::remove(const String& path) {
return FirebaseCall(host_, auth_, "DELETE", path, &http_);
}

Firebase& Firebase::stream(const String& path) {
_error.reset();
String url = makeURL(path);
const char* headers[] = {"Location"};
_http.setReuse(true);
_http.begin(_host.c_str(), firebasePort, url.c_str(), true, firebaseFingerprint);
_http.collectHeaders(headers, 1);
_http.addHeader("Accept", "text/event-stream");
int statusCode = _http.sendRequest("GET", (uint8_t*)NULL, 0);
String location;
// TODO(proppy): Add a max redirect check
while (statusCode == 307) {
location = _http.header("Location");
_http.setReuse(false);
_http.end();
_http.setReuse(true);
_http.begin(location, firebaseFingerprint);
statusCode = _http.sendRequest("GET", (uint8_t*)NULL, 0);
}
if (statusCode != 200) {
_error.set(statusCode,
"stream " + location + ": "
+ HTTPClient::errorToString(statusCode));
}
return *this;
FirebaseEventStream Firebase::stream(const String& path) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FirebaseStreamResult?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well since it is an interactive object it seemed like less of a "result" but this may be to your point of not naming any of them "result" because they may be interactive I. the future with laziness.

return FirebaseEventStream(host_, auth_, path);
}

String Firebase::makeURL(const String& path) {
String url;
if (path[0] != '/') {
url = "/";
}
url += path + ".json";
if (_auth.length() > 0) {
url += "?auth=" + _auth;
/* FirebaseCall */

FirebaseCall::FirebaseCall(const String& host, const String& auth,
const char* method, const String& path, const String& value,
HTTPClient* http) : http_(http) {
const String url = makeUrl(path, auth);
http_->begin(host.c_str(), kFirebasePort, url.c_str(), true, kFirebaseFingerprint);
status_ = http_->sendRequest(method, (uint8_t*)value.c_str(), value.length());
if (isError()) {
error_message_ = String(method) + " " + url + ": " + HTTPClient::errorToString(status_);
}
return url;
}

int Firebase::sendRequest(const char* method, const String& path, const String& value) {
String url = makeURL(path);
_http.begin(_host.c_str(), firebasePort, url.c_str(), true, firebaseFingerprint);
int statusCode = _http.sendRequest(method, (uint8_t*)value.c_str(), value.length());
setError(method, url, statusCode);
return statusCode;
FirebaseCall::FirebaseCall(const String& host, const String& auth,
const char* method, const String& path,
HTTPClient* http) : FirebaseCall(host, auth, method, path, "", http) {
}

String Firebase::sendRequestGetBody(const char* method, const String& path, const String& value) {
sendRequest(method, path, value);
if (_error.code() != 0) {
return "";
}
// no _http.end() because of connection reuse.
return _http.getString();
bool FirebaseCall::isOk() const {
return status_ == HTTP_CODE_OK;
}

bool FirebaseCall::isError() const {
return status_ < 0;
}

String FirebaseCall::errorMessage() const {
return error_message_;
}

void Firebase::setError(const char* method, const String& url, int statusCode) {
_error.reset();
if (statusCode < 0) {
_error.set(statusCode,
String(method) + " " + url + ": "
+ HTTPClient::errorToString(statusCode));
String FirebaseCall::rawResponse() {
return http_->getString();
}

/* FirebaseEventStream */

FirebaseEventStream::FirebaseEventStream(const String& host, const String& auth,
const String& path) {
const String url = makeUrl(path, auth);
http_.setReuse(true);
http_.begin(host.c_str(), kFirebasePort, url.c_str(), true,
kFirebaseFingerprint);
const char* headers[] = {"Location"};
http_.collectHeaders(headers, 1);
http_.addHeader("Accept", "text/event-stream");
status_ = http_.sendRequest("GET", (uint8_t*)NULL, 0);

String location;
// TODO(proppy): Add a max redirect check
while (status_ == HTTP_CODE_TEMPORARY_REDIRECT) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should do this in the stream() call so we can get some early failure, and keep the dedicated client only for the request after the last redirect.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems essentially the same. You will get the failure at the same time, when you check this object.

location = http_.header("Location");
http_.setReuse(false);
http_.end();
http_.setReuse(true);
http_.begin(location, kFirebaseFingerprint);
status_ = http_.sendRequest("GET", (uint8_t*)NULL, 0);
}

if (status_ != 200) {
error_message_ = "stream " + location + ": "
+ HTTPClient::errorToString(status_);
}
}

bool Firebase::connected() {
return _http.connected();
bool FirebaseEventStream::connected() {
return http_.connected();
}

bool Firebase::available() {
return _http.getStreamPtr()->available();
bool FirebaseEventStream::available() {
return http_.getStreamPtr()->available();
}

Firebase::Event Firebase::read(String& event) {
auto client = _http.getStreamPtr();
Event type;;
FirebaseEventStream::Event FirebaseEventStream::read(String& event) {
auto client = http_.getStreamPtr();
Event type;
String typeStr = client->readStringUntil('\n').substring(7);
if (typeStr == "put") {
type = Firebase::Event::PUT;
type = FirebaseEventStream::Event::PUT;
} else if (typeStr == "patch") {
type = Firebase::Event::PATCH;
type = FirebaseEventStream::Event::PATCH;
} else {
type = Firebase::Event::UNKNOWN;
type = FirebaseEventStream::Event::UNKNOWN;
}
event = client->readStringUntil('\n').substring(6);
client->readStringUntil('\n'); // consume separator
return type;
}

bool FirebaseEventStream::isError() const {
return status_ < 0;
}

String FirebaseEventStream::errorMessage() const {
return error_message_;
}


112 changes: 78 additions & 34 deletions Firebase.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,53 +25,97 @@
#include <WiFiClientSecure.h>
#include <ESP8266HTTPClient.h>

// FirebaseError represents a Firebase API error with a code and a
// message.
class FirebaseError {
public:
operator bool() const { return _code < 0; }
int code() const { return _code; }
const String& message() const { return _message; }
void reset() { set(0, ""); }
void set(int code, const String& message) {
_code = code;
_message = message;
}
private:
int _code = 0;
String _message = "";
};
//TODO(edcoyne) split these into multiple files.

class FirebaseCall;
class FirebaseEventStream;

// Firebase is the connection to firebase.
// Primary client to the Firebase backend.
class Firebase {
public:
Firebase(const String& host);
Firebase& auth(const String& auth);
const FirebaseError& error() const {
return _error;

// Fetch result at "path" to a local variable. If the value is too large you will exceed
// local memory.
FirebaseCall get(const String& path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another thing is that if we return a copy of the FirebaseCall object, user do have to worry about its lifetime:
i.e: if you do Firebase.get().rawResponse() it will discard the intermediate FirebaseCall object, I don't think that's necessary a problem but ideally I'd like user to be able to worry only about:

  • the Firebase type and its methods
  • extracting standard result type from the API method call (int, float, string, json).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with not handing the Call object back to the client is the lifetime of the call object is bound now to the lifetime of the Firebase object. One should be short lived and one long lived.

For example, if you call Firebase.get() then never read the response we can never clear the response from the buffer because we don't know if you will ask for it 3 hours later. However if the client owns the FirebaseCall object and deletes it, we can then clear the response from the buffer because it will never be read.

Let me ponder on it for a bit and see if there is a way we can get the ease of use that you want with the lifetime isolation I want.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can never clear the response from the buffer because we don't know if you will ask for it 3 hours later

I agree with this generally, but given that the interaction between the arduino sketch and the lib is not likely to be concurrent (1 request at a time for a given Firebase object). Do you still see this as an issue if we hold the buffer until the next request (and overwrite it)?.

The esp8266 http lib is currently modeled that way:
https://github.com/esp8266/Arduino/blob/0834ec62a1d7ac832026c33f6c6b97e1c76ebba8/libraries/ESP8266HTTPClient/src/ESP8266HTTPClient.h#L199 with a bunch of mutable state within the http client object that get reset on every request.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are a couple of issues with holding it even assuming non-concurrency. One is memory usage, on a memory constrained device having potentially large chunks of memory sitting unused can be an issue. The other is if I remember TCP/HTTP correctly than on a large request part of this will actually be held on the server so this could have larger effects. (i.e. if memory serves, i should verify, but if the client buffer is full the server will buffer the rest until the client resumes reading.


// Add new value to list at "path", will return child name of new item.
FirebaseCall push(const String& path, const String& value);

// Deletes value at "path" from server.
FirebaseCall remove(const String& path);

// Starts a stream of events that effect object at "path".
FirebaseEventStream stream(const String& path);

private:
HTTPClient http_;
String host_;
String auth_;
};

class FirebaseCall {
public:
FirebaseCall(const String& host, const String& auth,
const char* method, const String& path, const String& value,
HTTPClient* http);
FirebaseCall(const String& host, const String& auth,
const char* method, const String& path,
HTTPClient* http);


// True if there was an error completing call.
bool isError() const;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should keep the separate FirebaseError type, with a message() method, etc, and have API method specifics result types compose the generic error and the method specific result.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

String errorMessage() const;

// True if http status code is 200(OK).
bool isOk() const;

// Message sent back from Firebase backend. This pulls value to local memory,
// be careful if value can be large.
String rawResponse();

int httpStatus() const {
return status_;
}
String get(const String& path);
String push(const String& path, const String& value);
bool remove(const String& path);
bool connected();
Firebase& stream(const String& path);
bool available();

private:
FirebaseCall(HTTPClient* http);

HTTPClient* http_;

int status_;
String error_message_;
};

class FirebaseEventStream {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd love if we could keep the stream method attached to the main Firebase class (even if the implementation is decoupled).

If we decouple the method for making request and getting result it seems that most of the stream interface could apply to regular request, and the real different would be that you can read() multiple data from a stream.

ex: available() could also be useful for regular get request to know if there is a body to read.

public:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably allow a lazy construction of the FirebaseStream object (and even the other type) through a begin() method, that seems to be a popular pattern in the Arduino world, and would solve some of the lifecycle headache.

enum Event {
UNKNOWN,
PUT,
PATCH
};

FirebaseEventStream(const String& host, const String& auth, const String& path);

// Read next event in stream.
Event read(String& event);

// True if connected to backend.
bool connected();

// True if there is an event available.
bool available();

// True if there was an error.
bool isError() const;
String errorMessage() const;

private:
String makeURL(const String& path);
int sendRequest(const char* method, const String& path, const String& value = "");
String sendRequestGetBody(const char* method, const String& path, const String& value = "");
void setError(const char* method, const String& url, int status_code);

HTTPClient _http;
String _host;
String _auth;
FirebaseError _error;
HTTPClient http_;
int status_;
String error_message_;
};

#endif // firebase_h
20 changes: 12 additions & 8 deletions examples/FirebasePush_ESP8266/FirebasePush_ESP8266.ino
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,23 @@ void setup() {
Serial.print("connected: ");
Serial.println(WiFi.localIP());

// add a new entry.
String l = fbase.push("/logs", "{\".sv\": \"timestamp\"}");
// handle error.
if (fbase.error()) {
// add a new entry.
FirebaseCall push = fbase.push("/logs", "{\".sv\": \"timestamp\"}");
if (push.isError()) {
Serial.println("Firebase request failed");
Serial.println(fbase.error().message());
Serial.println(push.errorMessage());
return;
}

// print response.
Serial.println(l);
Serial.println(push.rawResponse());

// print all entries.
Serial.println(fbase.get("/logs"));
FirebaseCall fetch = fbase.get("/logs");
if (!fetch.isError()) {
Serial.println(fetch.rawResponse());
}
}

void loop() {
}
}
Loading