Dags för en lägesuppdatering! Version 1.0 av ringklockeplingaren är färdig och driftsatt. Det har varit väldigt lärorikt även om jag bara skrapat på ytan på flera områden.
Jag skrev om koden för Arduino-enheten i morse så att den går ner i energisparläge och vaknar på extern interrupt från antingen ringklockan(pin 2) eller inkommande meddelande på NRF24L01(pin 3).
Min android-app kan fortfarande förbättras, jag är inte något vidare på UI-design
. Men den gör vad den ska i alla fall. Man måste vara inloggad för att kunna ringa i klockan, jag har valt att godkänna inloggning via Google-konto så det görs i appen om man skulle vara utloggad. Därefter kan man trycka på knappen "PLINGPLONG" och efter en stund ringer det i ringklockan. I status-rutan kan man avsläsa om meddelandet gick fram till ringklockan eller om sändningen misslyckades.
När man trycker på ringklockan skickas en notifikation till min mobil med en timestamp. I framtiden funderar jag på att lägga till en rullista som sparar alla historiska ringningar.
Jag tycker att programmet i Raspberryn var absolut svårast att skriva. Jag programmerade lite i C för 10+ år sedan och skrev en del i JAVA förra året(körde bland annat Advent of Code). RF24 som är biblioteket för NRF24L01 är gjort skrivet i C++ och däför valde jag att skriva all kod i C++. Min känsla är att C++ är en kombination av de svåraste begreppen från C och JAVA. Jag har haft rejäla bekymmer med kodandet och det blir inte bättre av att libcurl som jag använder för att kommunicera med databasen är skrivet i C men accepterar C++ till viss del. En erfaren C++-kodare har nog en och annan synpunkt på min kod gissar jag. Jag överväger att skriva om allting i JAVA som har ett betydligt bättre stöd för firebase-kommunikation eftersom jag behärskar JAVA mycket bättre.
Nedan kommer koden till alla tre enheter, kanske finns det snuttar som andra kan ha nytta av?
Jag sitter i kicad och ritar på ett kort som jag tänker beställa från någon billig leverantör, det blir avslutningen på projektet. Jag har fler saker jag vill bygga så det kommer nog en tråd till inom kort.
Kod: Markera allt
#include <stdio.h>
#include <string.h>
#include <curl/curl.h>
#include <unistd.h>
#include <iostream>
#include <stdlib.h>
#include <RF24/RF24.h>
#include <time.h>
#include <chrono>
#include <thread>
#include <functional>
#include <ctime>
#include <sstream>
#include <iomanip>
#include <string>
// Ställ in parametrar för radio vid uppstart
RF24 radio(RPI_V2_GPIO_P1_22, RPI_V2_GPIO_P1_24, BCM2835_SPI_SPEED_8MHZ);
const uint64_t pipes[2] = {0xF0F0F0F0E1LL, 0xF0F0F0F0D2LL};
static char send_payload[] = "pling";
// sending notification
CURL *curl;
// checking status
CURL *curl2;
// ACK plinkplong
CURL *curl3;
std::string readBuffer;
CURLcode res;
/* Add a custom header */
struct curl_slist *chunk = NULL;
/* Hjälpfunktion för att infoga datum/tid i notifiering */
std::string notification() {
auto now = std::chrono::system_clock::now();
auto in_time_t = std::chrono::system_clock::to_time_t(now);
std::stringstream ss;
ss << "{\"to\":\"dhw7K_YHaBU:APA...K3Nfm09ZI2pke_4uCNOIxIGLBFV-y8q_UX6_\",\"notification\":{\"body\":\"DINGDONG ";
ss << std::put_time(std::localtime(&in_time_t), "%d/%m %X ");
ss << "\"},\"priority\":10}";
return ss.str();
}
/* Hjälpfunktion till libcurl */
static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) {
((std::string*)userp)->append((char*) contents, size * nmemb);
return size * nmemb;
}
// Körs en gång endast, när programmet startar
void setup(void) {
radio.begin();
radio.setRetries(15, 15);
radio.setPALevel(RF24_PA_MAX);
radio.openWritingPipe(pipes[1]);
radio.openReadingPipe(1, pipes[0]);
radio.setChannel(108);
radio.startListening();
// Skicka notifiering - POST
chunk = curl_slist_append(chunk, "Authorization: key=AAAAdf1aBrk:APA91b...mylY1P0aGpDtRywHVmM-CzutHP6s_eqVnJxSPd8w");
chunk = curl_slist_append(chunk, "Content-Type: application/json");
curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, "https://fcm.googleapis.com/fcm/send");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk);
}
// Läs status på ringklocka - GET
curl2 = curl_easy_init();
if (curl2) {
curl_easy_setopt(curl2, CURLOPT_URL, "https://ringklocka-ee170.firebaseio.com/ring/status.json");
curl_easy_setopt(curl2, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl2, CURLOPT_WRITEDATA, &readBuffer);
}
// Uppdatera värde på ringklocka - PATCH
curl3 = curl_easy_init();
if (curl3) {
curl_easy_setopt(curl3, CURLOPT_URL, "https://ringklocka-ee170.firebaseio.com/ring.json?auth=rxcgZ6TXilcSSHNnsY...gcbAeDB");
curl_easy_setopt(curl3, CURLOPT_CUSTOMREQUEST, "PATCH");
}
}
void timer_start(std::function<void(void) > func, unsigned int interval) {
std::thread([func, interval]() {
while (true) {
auto x = std::chrono::steady_clock::now() + std::chrono::milliseconds(interval);
func();
std::this_thread::sleep_until(x);
}
}).detach();
}
// kontrollera om RING/STATUS. Triggas av funktionen timer_start med jämna intervall
void check_firebase() {
std::cout << "Checking database value..." << std::endl;
if (curl2) {
res = curl_easy_perform(curl2);
/* Check for errors */
if (res != CURLE_OK) {
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
}
std::cout << readBuffer << std::endl;
if (readBuffer == "\"TRUE\"") {
std::cout << "PLING PLONG!!" << std::endl;
radio.stopListening();
if (radio.write(send_payload, 5)) {
if (curl3) {
curl_easy_setopt(curl3, CURLOPT_POSTFIELDS, "{ \"status\": \"DELIVERED\" }");
res = curl_easy_perform(curl3);
std::cout << "\n";
}
} else {
if (curl3) {
curl_easy_setopt(curl3, CURLOPT_POSTFIELDS, "{ \"status\": \"NO ACK. FROM NRF24L01+\" }");
res = curl_easy_perform(curl3);
std::cout << "\n";
}
}
radio.startListening();
} else if (readBuffer == "\"DELIVERED\"" || readBuffer == "\"NO ACK. FROM NRF24L01+\"") {
if (curl3) {
curl_easy_setopt(curl3, CURLOPT_POSTFIELDS, "{ \"status\": \"READY\" }");
res = curl_easy_perform(curl3);
std::cout << "\n";
}
} else {
std::cout << "FALSE" << std::endl;
}
readBuffer.clear();
}
}
int main(void) {
setup();
timer_start(check_firebase, 10000);
while (true) { // evig slinga
std::cout << "Checking if radio is available..." << std::endl;
if (radio.available()) {
int payload_size = radio.getDynamicPayloadSize();
if (payload_size > 1) {
char* payload = new char[payload_size + 1];
radio.read(payload, payload_size);
payload[payload_size] = '\0';
std::cout << "Got Message: " << payload << std::endl;
if (curl) {
std::string temp = notification();
char temp2[temp.length()];
strcpy(temp2, temp.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, temp.c_str());
res = curl_easy_perform(curl);
/* Check for errors */
if (res != CURLE_OK) {
fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
}
}
}
}
std::this_thread::sleep_for(std::chrono::milliseconds(5000)); // Vilar programmet 5 sekunder
}
/* always cleanup */
curl_easy_cleanup(curl);
curl_easy_cleanup(curl2);
curl_easy_cleanup(curl3);
curl_global_cleanup();
}
Kod: Markera allt
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
#include "printf.h"
#include <avr/sleep.h>
int plingPin = 2;
int irqPin = 3;
int relayPin = 8;
int plingStatus = HIGH;
RF24 radio(9, 10); // CE, CSN
const uint64_t pipes[2] = { 0xF0F0F0F0E1LL, 0xF0F0F0F0D2LL };
const char text[] = "PlingPlong!";
volatile bool plingSet = false;
volatile bool rxSet = false;
// Interrupt handler function - Tryck på ringklocka
void pling() {
sleep_disable();
detachInterrupt(digitalPinToInterrupt(plingPin));
detachInterrupt(digitalPinToInterrupt(irqPin));
plingSet = true;
}
// Interrupt handler function - Inkommande sändning
void rx() {
sleep_disable();
detachInterrupt(digitalPinToInterrupt(irqPin));
detachInterrupt(digitalPinToInterrupt(plingPin));
rxSet = true;
}
void setup() {
pinMode(plingPin, INPUT_PULLUP);
pinMode(irqPin, INPUT_PULLUP);
pinMode(relayPin, OUTPUT);
Serial.begin(9600);
// Arduino lyssnar på pipe 00001 och skriver på pipe 00002
radio.begin();
radio.openWritingPipe(pipes[0]);
radio.openReadingPipe(1, pipes[1]);
radio.setPALevel(RF24_PA_HIGH);
radio.setChannel(108);
//radio.setChannel(1);
radio.maskIRQ(1, 1, 0);
radio.startListening();
}
void sleepMode() {
Serial.println("DEBUG: In sleepMode-routine");
delay(500);
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
noInterrupts (); // make sure we don't get interrupted before we sleep
sleep_enable();
attachInterrupt(digitalPinToInterrupt(plingPin), pling, LOW);
attachInterrupt(digitalPinToInterrupt(irqPin), rx, LOW);
interrupts (); // interrupts allowed now, next instruction WILL be executed
sleep_cpu();
Serial.println("DEBUG: Waking up from interrupt");
}
void loop() {
// Någon har plingat på klockan
if (plingSet) {
radio.stopListening(); // Stoppa lyssning tillfälligt
radio.setPALevel(RF24_PA_HIGH);
Serial.println("DEBUG: Trying to send");
if (radio.write(&text, sizeof(text))) {
Serial.println("DEBUG: Send ok");
} else {
Serial.println("DEBUG: Send failed");
}
radio.startListening(); // Starta lyssning efter att meddelandet är skickat
delay(5000); // Låt det gå minst 5 sek innan vi skickar en ny pling
plingSet = false;
}
// RPi vill plinga på klockan
if (rxSet) {
if ( radio.available()) {
Serial.println("Radio available");
int payload_size = radio.getDynamicPayloadSize();
if (payload_size > 1) {
// Variable for the received timestamp
char* payload = new char[payload_size + 1];
radio.read(payload, payload_size);
Serial.print("Got this: ");
Serial.println(payload);
if (strcmp(payload, "pling") == 0) {
Serial.println("Driving relay on/off, on/off");
digitalWrite(relayPin, HIGH);
delay(500);
digitalWrite(relayPin, LOW);
delay(500);
digitalWrite(relayPin, HIGH);
delay(500);
digitalWrite(relayPin, LOW);
delay(500);
} else {
Serial.println("Payload != \"pling\"");
}
}
}
rxSet = false;
}
digitalWrite(relayPin, LOW);
delay(1000);
sleepMode();
}
Kod: Markera allt
package com.example.doorbell;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.firebase.ui.auth.AuthUI;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.InstanceIdResult;
import java.util.Arrays;
import java.util.List;
public class MainActivity extends AppCompatActivity {
TextView statusBox;
FirebaseDatabase database;
DatabaseReference myRef;
private int RC_SIGN_IN;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
database = FirebaseDatabase.getInstance();
FirebaseInstanceId.getInstance().getInstanceId()
.addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
@Override
public void onComplete(@NonNull Task<InstanceIdResult> task) {
if (!task.isSuccessful()) {
Log.w("tkn", "getInstanceId failed", task.getException());
return;
}
// Get new Instance ID token
String token = task.getResult().getToken();
DatabaseReference tkn = database.getReference("token");
tkn.setValue(token);
// Log and toast
String msg = getString(R.string.msg_token_fmt, token);
Log.d("tkn", msg);
//Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
}
});
View loginButton = findViewById(R.id.button3);
if (FirebaseAuth.getInstance().getCurrentUser() == null) {
loginButton.setEnabled(true);
} else {
loginButton.setEnabled(false);
}
// Write a message to the database
myRef = database.getReference("ring/status");
statusBox = findViewById(R.id.textView2);
myRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
String test = dataSnapshot.getValue(String.class);
statusBox.setText(test);
}
@Override
public void onCancelled(@NonNull DatabaseError databaseError) {
}
});
}
public void activate(View v) {
myRef.setValue("TRUE");
}
public void login(View v) {
// Choose authentication providers
List<AuthUI.IdpConfig> providers = Arrays.asList(
new AuthUI.IdpConfig.EmailBuilder().build(),
new AuthUI.IdpConfig.GoogleBuilder().build());
// Create and launch sign-in intent
startActivityForResult(
AuthUI.getInstance()
.createSignInIntentBuilder()
.setAvailableProviders(providers)
.build(),
RC_SIGN_IN);
}
}