Android TV Remote Control API [duplicate] - android
I have been tasked to create an application for android mobile to control an Android TV, preferably the dashboard/landingpage outside of any apps (settings included).
It doesn't really matter if it's via bluetooth or wifi, although I have found that bluetooth is not possible as the HID profile is needed, and that profile is only available on API 28 (I need to support from API 19 up)
There are some apps on the play store that already have this functionality. Most connect via Wifi to the Android TV, also pairing with it.
By analysing the APK Files I found out some options, i.e.
some use the
connectSDK library
others use what seems to be a native google package that I can't seem to find
import com.google.android.tv.support.remote.Discovery;
import com.google.android.tv.support.remote.core.Client;
import com.google.android.tv.remote.BuildInfo;
I found that a couple of years ago the Anymote Protocol could be used as well, but that one only works with Google TV, not Android TV.
The problems I am facing right now is that the connectSDK library isn't being maintained and does not contain any code for Android TV connections.
The native google package cannot be found anywhere, not sure if it's included in a specific Jar file, or maybe some obscured/hidden dependency?
I could try to create a connection to a specific socket with Android TV, I know for example that the ServiceType is "_androidtvremote._tcp." and that the port number is 6466. But I'm not sure what would be the best way to implement this.
What I'm looking for are some pointers or ideas how I could tackle this problem. Maybe some references as well.
EDIT on December 2021: I created a new documentation for the new protocol v2.
EDIT on September 2021: Google is deploying a new version of the "Android TV Remote Control" (from v4.x to v5), and this version is not compatible with the legacy pairing system. For now it's necessary to keep a version < 5 to make it work.
We spent some time to find how to connect and control an Android/Google TV (by reverse engineering), and I'm sharing here the result of our findings. For a more recent/updated version, you can check this wiki page.
I develop in PHP so I'll share the code in PHP (the Java code can be found by decompiling some Android apps using https://github.com/skylot/jadx)
Thanks to #hubertlejaune for his tremendous help.
The Android TV (aka server in this document) should have 2 open ports: 6466 and 6467.
To know more about the Android TV, we can enter the below Linux command:
openssl s_client -connect SERVER_IP:6467 -prexit -state -debug
Which will return some information, including the server's public certificate.
If you only want the server's public certificate:
openssl s_client -showcerts -connect SERVER_IP:6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem
Pairing
The pairing protocol will happen on port 6467.
Client's certificate
It's required to generate our own (client) certificate.
In PHP we can do it with the below code:
<?php
// the commande line is: php generate_key.php > client.pem
// certificate details (Distinguished Name)
// (OpenSSL applies defaults to missing fields)
$dn = array(
"commonName" => "atvremote",
"countryName" => "US",
"stateOrProvinceName" => "California",
"localityName" => "Montain View",
"organizationName" => "Google Inc.",
"organizationalUnitName" => "Android",
"emailAddress" => "example#google.com"
);
// create certificate which is valid for ~10 years
$privkey = openssl_pkey_new();
$cert = openssl_csr_new($dn, $privkey);
$cert = openssl_csr_sign($cert, null, $privkey, 3650);
// export public key
openssl_x509_export($cert, $out);
echo $out;
// export private key
$passphrase = null;
openssl_pkey_export($privkey, $out, $passphrase);
echo $out;
It will generate a file called client.pem that contains both the public and the private keys for our client.
Connection to the server
You need to open a TLS/SSL connection to the server using port 6467.
In PHP, you could use https://github.com/reactphp/socket:
<?php
use React\EventLoop\Factory;
use React\Socket\Connector;
use React\Socket\SecureConnector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/./vendor/autoload.php';
$host = 'SERVER_IP';
$loop = Factory::create();
$tcpConnector = new React\Socket\TcpConnector($loop);
$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop);
$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns);
$connector = new SecureConnector($dnsConnector, $loop, array(
'allow_self_signed' => true,
'verify_peer' => false,
'verify_peer_name' => false,
'dns' => false,
'local_cert' => 'client.pem'
));
$connector->connect('tls://' . $host . ':6467')->then(function (ConnectionInterface $connection) use ($host) {
$connection->on('data', function ($data) use ($connection) {
$dataLen = strlen($data);
echo "data recv => ".$data." (".strlen($data).")\n";
// deal with the messages received from the server
});
// below we can send the first message
$connection->write(/* first message here */);
}, 'printf');
$loop->run();
?>
Protocol
⚠️ Attention, each message is sent as a JSON string, but with two components/parts:
(first) we send the length of the message (JSON string) on 4 bytes,
(second) we send the message (JSON string) itself.
PAIRING_REQUEST(10)
As soon as we are connected to the server, we send a PAIRING_REQUEST(10) message (type = 10).
The first message to send is:
{"protocol_version":1,"payload":{"service_name":"androidtvremote","client_name":"CLIENT_NAME"},"type":10,"status":200}
The server returns a PAIRING_REQUEST_ACK(11) message with type is 11 and status is 200:
{"protocol_version":1,"payload":{},"type":11,"status":200}
OPTIONS(20)
Then the client replies with a OPTIONS(20) message (type = 20):
{"protocol_version":1,"payload":{"output_encodings":[{"symbol_length":4,"type":3}],"input_encodings":[{"symbol_length":4,"type":3}],"preferred_role":1},"type":20,"status":200}
The server returns a OPTIONS(20) message with type is 20 and status is 200.
CONFIGURATION(30)
Then the client replies with a CONFIGURATION(30) message (type = 30):
{"protocol_version":1,"payload":{"encoding":{"symbol_length":4,"type":3},"client_role":1},"type":30,"status":200}
The server returns a CONFIGURATION_ACK(31) message with type is 31 and status is 200.
🎉 The code appears on the TV screen!
SECRET(40)
Then the client replies with a SECRET(40) message (type = 40):
{"protocol_version":1,"payload":{"secret":"encodedSecret"},"type":40,"status":200}
At this stage, the TV screen shows a code with 4 characters (e.g. 4D35).
To find the encodedSecret:
we use a SHA-256 hash;
we add the client public key's modulus to the hash;
we add the client public key's exponent to the hash;
we add the server public key's modulus to the hash;
we add the server public key's exponent to the hash;
we add the last 2 characters of the code to the hash (in the example it's 35).
The result of the hash is then encoded in base64.
The server returns a SECRET_ACK(41) message with type is 41 and status is 200, as well as an encoded secret that permits to verify – we didn't try to decode it, but it's probably the first 2 characters of the code:
{"protocol_version":1,"payload":{"secret":"encodedSecretAck"},"type":41,"status":200}
PHP Code
(you can find some Java code that produces pretty much the same)
Here is the related PHP code:
<?php
use React\EventLoop\Factory;
use React\Socket\Connector;
use React\Socket\SecureConnector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/./vendor/autoload.php';
$host = 'SERVER_IP';
$loop = Factory::create();
$tcpConnector = new React\Socket\TcpConnector($loop);
$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop);
$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns);
// get the server's public certificate
exec("openssl s_client -showcerts -connect ".escapeshellcmd($host).":6467 </dev/null 2>/dev/null|openssl x509 -outform PEM > server.pem");
$connector = new SecureConnector($dnsConnector, $loop, array(
'allow_self_signed' => true,
'verify_peer' => false,
'verify_peer_name' => false,
'dns' => false,
'local_cert' => 'client.pem'
));
// return the message's length on 4 bytes
function getLen($len) {
return chr($len>>24 & 0xFF).chr($len>>16 & 0xFF).chr($len>>8 & 0xFF).chr($len & 0xFF);
}
// connect to the server
$connector->connect('tls://' . $host . ':6467')->then(function (ConnectionInterface $connection) use ($host) {
$connection->on('data', function ($data) use ($connection) {
$dataLen = strlen($data);
echo "data recv => ".$data." (".strlen($data).")\n";
// the first response from the server is the message's size on 4 bytes (that looks like a char to convert to decimal) – we can ignore it
// only look at messages longer than 4 bytes
if ($dataLen > 4) {
// decode the JSON string
$res = json_decode($data);
// check the status is 200
if ($res->status === 200) {
// check at which step we are
switch($res->type) {
case 11:{
// message to send:
// {"protocol_version":1,"payload":{"output_encodings":[{"symbol_length":4,"type":3}],"input_encodings":[{"symbol_length":4,"type":3}],"preferred_role":1},"type":20,"status":200}
$json = new stdClass();
$json->protocol_version = 1;
$json->payload = new stdClass();
$json->payload->output_encodings = [];
$encoding = new stdClass();
$encoding->symbol_length = 4;
$encoding->type = 3;
array_push($json->payload->output_encodings, $encoding);
$json->payload->input_encodings = [];
$encoding = new stdClass();
$encoding->symbol_length = 4;
$encoding->type = 3;
array_push($json->payload->input_encodings, $encoding);
$json->payload->preferred_role = 1;
$json->type = 20;
$json->status = 200;
$payload = json_encode($json);
$payloadLen = strlen($payload);
$connection->write(getLen($payloadLen));
$connection->write($payload);
break;
}
case 20:{
// message to send:
// {"protocol_version":1,"payload":{"encoding":{"symbol_length":4,"type":3},"client_role":1},"type":30,"status":200}
$json = new stdClass();
$json->protocol_version = 1;
$json->payload = new stdClass();
$json->payload->encoding = new stdClass();
$json->payload->encoding->symbol_length = 4;
$json->payload->encoding->type = 3;
$json->payload->client_role = 1;
$json->type = 30;
$json->status = 200;
$payload = json_encode($json);
$payloadLen = strlen($payload);
$connection->write(getLen($payloadLen));
$connection->write($payload);
break;
}
case 31:{
// when we arrive here, the TV screen displays a code with 4 characters
// message to send:
// {"protocol_version":1,"payload":{"secret":"encodedSecret"},"type":40,"status":200}
$json = new stdClass();
$json->protocol_version = 1;
$json->payload = new stdClass();
// get the code... here we'll let the user to enter it in the console
$code = readline("Code: ");
// get the client's certificate
$clientPub = openssl_get_publickey(file_get_contents("client.pem"));
$clientPubDetails = openssl_pkey_get_details($clientPub);
// get the server's certificate
$serverPub = openssl_get_publickey(file_get_contents("public.key"));
$serverPubDetails = openssl_pkey_get_details($serverPub);
// get the client's certificate modulus
$clientModulus = $clientPubDetails['rsa']['n'];
// get the client's certificate exponent
$clientExponent = $clientPubDetails['rsa']['e'];
// get the server's certificate modulus
$serverModulus = $serverPubDetails['rsa']['n'];
// get the server's certificate exponent
$serverExponent = $serverPubDetails['rsa']['e'];
// use SHA-256
$ctxHash = hash_init('sha256');
hash_update($ctxHash, $clientModulus);
hash_update($ctxHash, $clientExponent);
hash_update($ctxHash, $serverModulus);
hash_update($ctxHash, $serverExponent);
// only keep the last two characters of the code
$codeBin = hex2bin(substr($code, 2));
hash_update($ctxHash, $codeBin);
$alpha = hash_final($ctxHash, true);
// encode in base64
$json->payload->secret = base64_encode($alpha);
$json->type = 40;
$json->status = 200;
$payload = json_encode($json);
$payloadLen = strlen($payload);
$connection->write(getLen($payloadLen));
$connection->write($payload);
break;
}
}
}
}
});
// send the first message to the server
// {"protocol_version":1,"payload":{"service_name":"androidtvremote","client_name":"TEST"},"type":10,"status":200}
$json = new stdClass();
$json->protocol_version = 1;
$json->payload = new stdClass();
$json->payload->service_name = "androidtvremote";
$json->payload->client_name = "interface Web";
$json->type = 10;
$json->status = 200;
$payload = json_encode($json);
$payloadLen = strlen($payload);
// send the message size
$connection->write(getLen($payloadLen));
// send the message
$connection->write($payload);
}, 'printf');
$loop->run();
?>
Send Commands
Now that the client is paired with the server, we'll use port 6466 to send the commands.
Please, note we'll use an array of bytes for the commands.
Configuration message
An initial message must be sent:
[1,0,0,21,0,0,0,1,0,0,0,1,32,3,0,0,0,0,0,0,4,116,101,115,116]
The server will respond with an array of bytes that should start with [1,7,0
Commands
You must send two messages to execute one command.
The format is:
[1,2,0,{SIZE=16},0,0,0,0,0,0,0, {COUNTER} ,0,0,0, {PRESS=0} ,0,0,0,{KEYCODE}]
[1,2,0,{SIZE=16},0,0,0,0,0,0,0,{COUNTER+1},0,0,0,{RELEASE=1},0,0,0,{KEYCODE}]
The {KEYCODE} can be found on https://developer.android.com/reference/android/view/KeyEvent.
For example, if we want to send a VOLUME_UP:
[1,2,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24]
[1,2,0,16,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,24]
PHP Code
And here some PHP code:
<?php
use React\EventLoop\Factory;
use React\Socket\Connector;
use React\Socket\SecureConnector;
use React\Socket\ConnectionInterface;
require __DIR__ . '/./vendor/autoload.php';
$host = 'SERVER_IP';
$loop = Factory::create();
$tcpConnector = new React\Socket\TcpConnector($loop);
$dnsResolverFactory = new React\Dns\Resolver\Factory();
$dns = $dnsResolverFactory->createCached('8.8.8.8', $loop);
$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns);
$connector = new SecureConnector($dnsConnector, $loop, array(
'allow_self_signed' => true,
'verify_peer' => false,
'verify_peer_name' => false,
'dns' => false,
'local_cert' => 'client.pem'
));
// convert the array of bytes
function toMsg($arr) {
$chars = array_map("chr", $arr);
return join($chars);
}
// connect to the server
$connector->connect('tls://' . $host . ':6466')->then(function (ConnectionInterface $connection) use ($host) {
$connection->on('data', function ($data) use ($connection) {
// convert the data received to an array of bytes
$dataLen = strlen($data);
$arr = [];
for ($i=0; $i<$dataLen;$i++) {
$arr[] = ord($data[$i]);
}
$str = "[".implode(",", $arr)."]";
echo "data recv => ".$data." ".$str." (".strlen($data).")\n";
// if we receive [1,20,0,0] it means the server sent a ping
if (strpos($str, "[1,20,0,0]") === 0) {
// we can reply with a PONG [1,21,0,0] if we want
// $connection->write(toMsg([1,21,0,0]));
}
else if (strpos($str, "[1,7,0,") === 0) {
// we can send the command, here it's a VOLUME_UP
$connection->write(toMsg([1,2,0,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24]));
$connection->write(toMsg([1,2,0,16,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,24]));
}
});
// send the first message (configuration) to the server
$arr = [1,0,0,21,0,0,0,1,0,0,0,1,32,3,0,0,0,0,0,0,4,116,101,115,116];
$connection->write(toMsg($arr));
}, 'printf');
$loop->run();
?>
So, I found the answer what I was looking for.
If you are a Google Partner (and only then), and have an account with those privileges, you can simply download the jar file at this location. Documentation can be found there as well and the SDK exists for Android and iOS.
Not much information is available how to use it. But by looking over the different classes it can become clear.
Related
Need help sending wifi credentials (ssid and password) from ionic app for Android to esp32
Trying to send esp32 chip wifi credentials from android app (built with ionic). Using Ionic capacitor/bluetooth-le plug in to write to esp32, using the write function: await BleClient.write(device.deviceId, GATT Service ID, Characteristic ID, textToDataView('wifi_ssid,wifi_password')); Code for BleClient.write: async write(deviceId: string, service: string, characteristic: string, value: DataView): Promise<void> { service = validateUUID(service); characteristic = validateUUID(characteristic); return this.queue(async () => { if (!value?.buffer) { throw new Error('Invalid data.'); } let writeValue: DataView | string = value; if (Capacitor.getPlatform() !== 'web') { // on native we can only write strings writeValue = dataViewToHexString(value); } await BluetoothLe.write({ deviceId, service, characteristic, value: writeValue, }); }); } How to pass wifi credentials as value argument to the write function so it's correctly received by ESP32? On the ESP32 side, I'm using the wifi_prov_mgr example code, which in turn uses google protocol buffer (I'm very new to protobuf and don't really understand how it works). ESP uses wifi_config.c (Espressif wifi_provisioning component). When I send wifi credentials from the app using BleClient.write, it shows up in wifi_config.c as inbuf with value wifi_ssid,wifi_password:��Z�?�Z�? Here's the relevant code for wifi_config.c: esp_err_t wifi_prov_config_data_handler(uint32_t session_id, const uint8_t *inbuf, ssize_t inlen, uint8_t **outbuf, ssize_t *outlen, void *priv_data) { ESP_LOGI(TAG ,"Wifi config payload inbuf value: %s", inbuf); WiFiConfigPayload *req; WiFiConfigPayload resp; esp_err_t ret; req = wi_fi_config_payload__unpack(NULL, inlen, inbuf); if (!req) { ESP_LOGE(TAG, "Unable to unpack config data"); return ESP_ERR_INVALID_ARG; } I'm having a very hard time understanding how wi_fi_config_payload_unpack processes the inbuf argument. This is where the code gets into protobuf code generated by the proto files. The proto file for wifi config data looks like this: message CmdSetConfig { bytes ssid = 1; bytes passphrase = 2; bytes bssid = 3; int32 channel = 4; } message WiFiConfigPayload { WiFiConfigMsgType msg = 1; oneof payload { CmdGetStatus cmd_get_status = 10; RespGetStatus resp_get_status = 11; CmdSetConfig cmd_set_config = 12; RespSetConfig resp_set_config = 13; CmdApplyConfig cmd_apply_config = 14; RespApplyConfig resp_apply_config = 15; } } So my question is - how do I pass the wifi credentials in BleClient.write so it is correctly recognized by wifi_config.c on the ESP32 side? I thought about using the Android app developed by Espressif to pass wifi credentials to the ESP32 chip, but then I don't know how to integrate native Android code with ionic code, since I need my app to do more than just credential the ESP32.
Resolved this issue by using Google's protocol buffer code for javascript. Here's code that worked for me: import * as goog from 'google-protobuf'; export class SetupPage implements OnInit { messages = require('../../assets/proto-js/wifi_config_pb.js'); connectedDevice: BleDevice; bleScan: any; wifiSSID: Promise<any>; scanResults: Promise<[]>; wifiCredentials: string; uint8String: Uint8Array; buffer: Uint8Array; ngOnInit() { async getBLE(){ await BleClient.initialize(); var cmdSetMessage = new this.messages.CmdSetConfig(); var wifiConfigPayloadMessage = new this.messages.WiFiConfigPayload; message.setSsid('wifiid'); message.setPassphrase('password'); wifiConfigPayloadMessage.setCmdSetConfig(cmdSetMessage); wifiConfigPayloadMessage.setMsg(2); //this is ESP32 BLE specific let bytesOfStuff = await wifiConfigPayloadMessage.serializeBinary(); this.connectedDevice = await BleClient.requestDevice(); await BleClient.connect(this.connectedDevice.deviceId); BleClient.writeWithoutResponse(this.connectedDevice.deviceId, '021a9004-0382- 4aea-bff4-6b3f1c5adfb4', '021aff52-0382-4aea-bff4-6b3f1c5adfb4', bytesOfStuff) //After this, you need to send wifiConfigPayloadMessage.setMsg(4) to ESP32 to apply wifi credentials and connect to the selected wifi } } Before this code can function, you need to install google-protobuf from npm or other package installer and protoc or another compiler for the proto files in order to generate pb.js files (since I'm working with javascript) for each proto file. In this example, the proto file is called wifi_config.proto, and the corresponding pb.js file (created by running protoc) is called wifi_config_pb.js. Functions used to create the data to be transferred are defined in the pb.js files and correspond to objects defined in the proto files. The following references will help a lot if you don't have a good understanding of how this mechanism works: https://developers.google.com/protocol-buffers/docs/proto3 (google protocol-buffer tutorial) Protocol Buffers in Ionic (stack question)
How do I implement JWT with pub sub push
I followed the documentation on pub/sub notifications with the push method here And I want to have authentication on my call with JWT. I looked at their GitHub example here app.post('/pubsub/authenticated-push', jsonBodyParser, async (req, res) => { // Verify that the request originates from the application. if (req.query.token !== PUBSUB_VERIFICATION_TOKEN) { res.status(400).send('Invalid request'); return; } // Verify that the push request originates from Cloud Pub/Sub. try { // Get the Cloud Pub/Sub-generated JWT in the "Authorization" header. const bearer = req.header('Authorization'); const [, token] = bearer.match(/Bearer (.*)/); tokens.push(token); // Verify and decode the JWT. // Note: For high volume push requests, it would save some network // overhead if you verify the tokens offline by decoding them using // Google's Public Cert; caching already seen tokens works best when // a large volume of messages have prompted a single push server to // handle them, in which case they would all share the same token for // a limited time window. const ticket = await authClient.verifyIdToken({ idToken: token, audience: 'example.com', }); const claim = ticket.getPayload(); claims.push(claim); } catch (e) { res.status(400).send('Invalid token'); return; } // The message is a unicode string encoded in base64. const message = Buffer.from(req.body.message.data, 'base64').toString( 'utf-8' ); messages.push(message); res.status(200).send(); }); But I have some questions. What is the PUBSUB_VERIFICATION_TOKEN and how do I get it and store it in my environment? const [, token] = bearer?.match(/Bearer (.*)/); throws the following error Type 'RegExpMatchArray | null | undefined' must have a 'Symbol.iterator' method that returns an iterator.ts(2488) Why do they push the claims and tokens in an array if they never check that array in this function for already existing tokens / claims? I am trying to implement this with a Firebase Cloud Function and this is what I have. Is it even possible to cache the tokens / claims? //Service account auth client const authClient = new google.auth.JWT({ email: android_key.client_email, key: android_key.private_key, scopes: ["https://www.googleapis.com/auth/androidpublisher"] }); export const handlePubSub = functions.region('europe-west1').https.onRequest(async (req, res) => { // What is PUBSUB_VERIFICATION_TOKEN??? if (req.query.token !== PUBSUB_VERIFICATION_TOKEN) { res.status(400).send('Invalid request'); return; } try { const bearer = req.header('Authorization'); const [, token] = bearer?.match(/Bearer (.*)/); //Error Type 'RegExpMatchArray | null | undefined' must have a 'Symbol.iterator' method that returns an iterator.ts(2488) tokens.push(token); // Why do this? Can I do this in firebase cloud functions const ticket = await authClient.verifyIdToken({ idToken: token, }); const claim = ticket.getPayload(); claims.push(claim); // Why do this? Can I do this in firebase cloud functions } catch (e) { res.status(400).send('Invalid token'); return; } const message = Buffer.from(req.body.message.data, 'base64').toString( 'utf-8' ); console.log(message); return res.status(200).json({ statusCode: 200, method: req.method, message: 'Recieved successfully' }); });
What is the PUBSUB_VERIFICATION_TOKEN and how do I get it and store it in my environment? PUBSUB_VERIFICATION_TOKEN can be any value you want. Easiest way to set an environment variable is on the command line when running node: PUBSUB_VERIFICATION_TOKEN=whatevertoken node app.js The req.query.token that is compared too comes from the URL query string. GET /whatever?token=whatevertoken Type 'RegExpMatchArray | null | undefined' must have a 'Symbol.iterator' method that returns an iterator.ts(2488) That's a bug in their code. bearer.match can return undefined/null which can't be spread into the array [, token]. The example will only work when there is a successful regex match. This will parse in plain javascript but typescript highlights this issue at compile time. const bearer = req.header('Authorization'); const m = /Bearer (.*)/.exec(bearer) if (m) tokens.push(m[1]) Why do they push the claims and tokens in an array if they never check that array in this function for already existing tokens / claims? The example comments // List of all messages received by this instance. So more a debug store than something functional.
How to fix this Firebase message format sent from ESP32 (Arduino, ESP-IDF - all text, no Firebase library used)
I'm trying to get this code below to work which was working perfectly a year ago the last time I tried it. After running it I receive no notification in my app. Using in Arduino IDE on ESP32 module. No changes were made at all to the sketch that was once working other than updating the token. I do not get the "firebase error" message in the serial output so assuming no error. WiFiClient client; String serve = "MY SERVER KEY"; String appToken = "MY APP TOKEN"; String data = "{"; data = data + "\"to\": \"" + appToken + "\","; data = data + "\"notification\": {"; data = data + "\"body\": \"example body\","; data = data + "\"title\" : \"my title\" "; data = data + "} }"; Serial.println("Send data..."); if (client.connect("fcm.googleapis.com", 80)) { Serial.println("Connected to the server.."); client.println("POST /fcm/send HTTP/1.1"); client.println("Authorization: key=" + serve + ""); client.println("Content-Type: application/json"); client.println("Host: fcm.googleapis.com"); client.print("Content-Length: "); client.println(data.length()); client.print("\n"); client.print(data); Serial.println("data"); Serial.println(data); } else { Serial.println("firebase error"); } Serial.println("Data sent...Reading response.."); while (client.available()) { char c = client.read(); Serial.print(c); } Serial.println("Finished!"); client.flush(); client.stop(); } I just updated Firebase in my app and migrated to AndroidX and can receive messages sent from the Firebase console and I'm currently using this library successfully to send and receive the notifications in my app. Below is the example I'm using and it's working perfectly. #include <WiFi.h> #include <FirebaseESP32.h> #define WIFI_SSID "YOUR_WIFI_AP" #define WIFI_PASSWORD "YOUR_WIFI_PASSWORD" #define FIREBASE_HOST "YOUR_FIREBASE_PROJECT.firebaseio.com" //Do not include https:// in FIREBASE_HOST #define FIREBASE_AUTH "YOUR_FIREBASE_DATABASE_SECRET" #define FIREBASE_FCM_SERVER_KEY "YOUR_FIREBASE_PROJECT_CLOUD_MESSAGING_SERVER_KEY" #define FIREBASE_FCM_DEVICE_TOKEN_1 "RECIPIENT_DEVICE_TOKEN" #define FIREBASE_FCM_DEVICE_TOKEN_2 "ANOTHER_RECIPIENT_DEVICE_TOKEN" FirebaseData firebaseData1; unsigned long lastTime = 0; int count = 0; void sendMessage(); void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print("Connecting to Wi-Fi"); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(300); } Serial.println(); Serial.print("Connected with IP: "); Serial.println(WiFi.localIP()); Serial.println(); Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH); Firebase.reconnectWiFi(true); firebaseData1.fcm.begin(FIREBASE_FCM_SERVER_KEY); firebaseData1.fcm.addDeviceToken(FIREBASE_FCM_DEVICE_TOKEN_1); firebaseData1.fcm.addDeviceToken(FIREBASE_FCM_DEVICE_TOKEN_2); firebaseData1.fcm.setPriority("high"); firebaseData1.fcm.setTimeToLive(1000); sendMessage(); } void loop() { if (millis() - lastTime > 60 * 1000) { lastTime = millis(); sendMessage(); } } void sendMessage() { Serial.println("------------------------------------"); Serial.println("Send Firebase Cloud Messaging..."); firebaseData1.fcm.setNotifyMessage("Notification", "Hello World! " + String(count)); firebaseData1.fcm.setDataMessage("{\"myData\":" + String(count) + "}"); //if (Firebase.broadcastMessage(firebaseData1)) //if (Firebase.sendTopic(firebaseData1)) if (Firebase.sendMessage(firebaseData1, 0))//send message to recipient index 0 { Serial.println("PASSED"); Serial.println(firebaseData1.fcm.getSendResult()); Serial.println("------------------------------------"); Serial.println(); } else { Serial.println("FAILED"); Serial.println("REASON: " + firebaseData1.errorReason()); Serial.println("------------------------------------"); Serial.println(); } count++; } I've tried sending the code at the top in data and notification message format with app in foreground and background and cannot receive a message. I'm wondering if something in the Firebase format or rules or such has changed within the last year. I need to use the code at the top instead of the library because I can just add a few more key value pairs in the message body and also send to iOS which I have done successfully in the past using the same code. I'm sure the key pairs could be added with the library actually which I'm working on now but would really prefer the simplicity of the top code. Would appreciate any advice.
I'm not certain but I believe the problem may be that the Arduino code is sending via HTTP and not HTTPS, which I read in the FB docs HTTPS is required. Maybe they changed that because this same code was working perfectly for me a year ago. But I was in the process of migrating my code over to ESP-IDF and this function below is working on that with no problem which has slight mods to comply with C++ I'm using in PlatformIO / VS Code IDE. This was the only thing changed: esp_http_client_config_t config = {}; config.url = "https://fcm.googleapis.com/fcm/send"; config.event_handler = _http_event_handler; I didn't need any type of SSL certificate, I just sent the code as shown. I didn't try messing around too much with the Arduino code for HTTPS. static void firebasePost() { esp_http_client_config_t config = {}; // important to initialize with "{}" when using C++ on ESP-IDF http client or it will crash easily config.url = "https://fcm.googleapis.com/fcm/send"; config.event_handler = _http_event_handler; esp_http_client_handle_t client = esp_http_client_init(&config); esp_err_t err = esp_http_client_perform(client); const char *post_data = "{\"to\": \"eCiC-20m8Zw:APA91bE4i1rkC(SHORTENED)9JZpbW3gFe5Qfz9BhOFmqua3aeZoDZEQ\",\"notification\": {\"body\": \"Sample Body\",\"title\" : \"Sample Title\"} }"; esp_http_client_set_header(client, "Authorization", "key=AAAAZrM4XXXX:APA91bFnSr_U15y6mX(SHORTENED)WqaWECxYWaCf_rVPE"); esp_http_client_set_header(client, "Content-Type", "application/json"); esp_http_client_set_method(client, HTTP_METHOD_POST); esp_http_client_set_post_field(client, post_data, strlen(post_data)); err = esp_http_client_perform(client); if (err == ESP_OK) { ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %d", esp_http_client_get_status_code(client), esp_http_client_get_content_length(client)); } else { ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(err)); } esp_http_client_cleanup(client); }
The Arduino Firebase library connects to Firebase via SSL port 443 (HTTPS method) for both FCM and RTDB. Your above assumption is not correct. Your device token is invalid or not existed. You don't have to know the code inside the Arduino library. Google only accept secure connection for their services. The problems can be the device uid or redundant of FCM payload data. You accept your answer with your own assumption. No solution for this issue. You need to open the issue at GitHub repo.
How to implement GCM HTTP server in Python while avoiding my server's IP being blacklisted by Google?
I'm using Apache, WSGI (mod_wsgi) and Python, to implement a GCM HTTP server as describe in the Android Developer website: developer.android.com/google/gcm/server.html At first the code I've implemented on the server side to handle message sending to GCM was as the following: def send_to_gcm(data): url = 'https://android.googleapis.com/gcm/send' no = 1 while True: try: request = Request(url=url, data=json.dumps(data)) request.add_header('Authorization','key=AIzXXX') request.add_header('Content-Type', 'application/json') res = urlopen(request) if res.getcode() == 200: return except Exception: pass no += 1 #Discard the message if no == 16: return #Exponential backoff tts = randint(2**(no-1), (2**no) -1) sleep(tts) data = dict(registration_id=[regid], data=dict(mymessage=themessage)) thread = Thread(target=send_to_gcm, args=(data,)) thread.start() After a while (about a day) GCM stopped to accept the messages sent by the Server. So I started to dig here and there in the documentation of GCM and I found an important part of the specification I missed before: developer.android.com/google/gcm/http.html#response "Honor the Retry-After header if it's included in the response from the GCM server. ... Senders that cause problems risk being blacklisted. ... Happens when the HTTP status code is between 501 and 599, or when the error field of a JSON object in the results array is Unavailable." So i patched my server code as follow: def send_to_gcm(data, environ): url = 'https://android.googleapis.com/gcm/send' no = 1 while True: try: request = Request(url=url, data=json.dumps(data)) request.add_header('Authorization','key=AIzXXX') request.add_header('Content-Type', 'application/json') res = urlopen(request) if res.getcode() == 200: return except HTTPError as error: if error.headers.has_key('Retry-After'): try: tts = int(response_headers['Retry-After']) except ValueError: until = datetime.strptime(response_headers, '%a, %d %b %Y %H:%M:%S GMT') diff = until - datetime.now() tts = int(diff.total_seconds()) +1 sleep(tts) no += 1 #Discard the message if no == 16: return #Exponential backoff tts = randint(2**(no-1), (2**no) -1) sleep(tts) But actually it's likely my server has been blacklisted and for any request sent I receive a 401 status code and an "Unauthorized" error message. Here my questions: Is there something wrong in my latest server implementation? Will the static IP address of my server be unbanned and if yes when?
I was searching for the same subject. This module may help you https://github.com/geeknam/python-gcm
C2DM with App Engine Python returns 401 error
I'm tyring to send a message to my mobile. Via browser I call the method that does this operation, I've logged the registrationId, authToken, etc.. and this is correct, because I tested in a local server and the message has been send to my phone using these keys. However on App Engine, I have a 401 error on the result of the urlfetch.fetch for 'https://android.clients.google.com/c2dm/send'. Or if this is a problem with authentication. I doubt it is the problem above, because the method is called, and the error happens right in the end of the method in my App Engine server. Here is how I make the request to the C2DM servers: params = { 'registration_id':registrationId, 'collapse_key':0, 'data.payload':encoded_msg } paramsByte = urllib.urlencode(params) logging.info(registrationId) url = 'https://android.clients.google.com/c2dm/send' logging.info(token) result = urlfetch.fetch(url=url, payload=paramsByte, method=urlfetch.POST, headers={'Content-Type':'application/x-www-form-urlencoded', 'Authorization':'GoogleLogin auth='+token} ) Any help would be appreciated. Thanks. UPDATE Now the client is running in a hosting server as suggested, and the 401 error happens when contacting 'https://android.clients.google.com/c2dm/send'. However when using the following command on terminal with the same token and regId, it works. curl --header "Authorization: GoogleLogin auth=your_authenticationid" "https://android.apis.google.com/c2dm/send" -d registration_id=your_registration -d "data.payload=payload" -d collapse_key=0 Client code calling the method in server: $.getJSON('http://myapp.appspot.com/method?userId='+userId+'&message='+theMessage+'&callback=?', function(data) { console.log(data); }); Full method code for server: class PushHandler(webapp.RequestHandler): '''This method sends the message to C2DM server to send the message to the phone''' def get(self): logging.info('aqui dentro') userId = self.request.get('userId') message = self.request.get('message') callback = self.request.get('callback') token = getToken(self) #this is a method I've implemented to get the token from C2DM servers by passing the SenderId and Password registrationId = '' contactNumber = '' # Get the registrationId to send to the C2DM server to know which # device it may send the message regQuery = C2DMUser.all() regQuery.filter('userId =', int(userId)) for k in regQuery: registrationId = k.registrationId # Builds the json to be sent to the phone record_to_json = { 'userId':userId, 'message':message } data = [] data.append(record_to_json) jsondata = simplejson.dumps(data) # Creates the json # Encode the JSON String u = unicode(jsondata, "utf-8") encoded_msg = u.encode("utf-8") params = { 'registration_id':registrationId, 'collapse_key':0, 'data.payload':encoded_msg } paramsByte = urllib.urlencode(params) url = 'https://android.clients.google.com/c2dm/send' logging.info(token) result = urlfetch.fetch(url=url, payload=paramsByte, method=urlfetch.POST, headers={'Content-Type':'application/x-www-form-urlencoded', 'Authorization':'GoogleLogin auth='+token} ) data = [] params_key = { 'status_code':result.status_code } data.append(params_key) self.response.headers['Content-Type'] = 'application/json' jsondata = simplejson.dumps(data) if result.status_code == 200: logging.info(result.status_code) self.response.out.write('' + callback + '(' + jsondata + ')') # handle the JSONP else: logging.info(result.status_code) self.response.out.write(result.status_code)
The package name of your code must match the one you gave when you signed up for the c2dm account. For Java, if you gave com.myapp when you signed up, your c2dm calls must occur within that package. Not sure how this translates to Python, though.
As far as the C2DM part is concerned, everything seems okay. If you are saying that with the same credentials it works with your local server, I am guessing that it should work on App Engine. As far as the XMLHttpRequest error is concerned, you can't issue requests through the XMLHttpRequest to other domains or sub-domains. So, you cannot issue your request from localhost to yourSite. A solution would be using JSONP.