From 76edd6c448c55c4f9bcf37509ec372149c966993 Mon Sep 17 00:00:00 2001 From: schneefux Date: Fri, 25 Dec 2015 13:38:31 +0100 Subject: first commit --- AndroidClient/AmbientNotifications.iml | 21 ++ AndroidClient/README.md | 1 + AndroidClient/app/.gitignore | 1 + AndroidClient/app/app.iml | 91 +++++++ AndroidClient/app/build.gradle | 28 ++ AndroidClient/app/proguard-rules.pro | 17 ++ AndroidClient/app/src/main/AndroidManifest.xml | 34 +++ AndroidClient/app/src/main/ic_launcher-web.png | Bin 0 -> 16925 bytes .../notifications/NotifListenerService.java | 129 +++++++++ .../snowman/notifications/WebViewActivity.java | 17 ++ .../apptheme_scrubber_control_disabled_holo.png | Bin 0 -> 883 bytes .../apptheme_scrubber_control_focused_holo.png | Bin 0 -> 1096 bytes .../apptheme_scrubber_control_normal_holo.png | Bin 0 -> 1342 bytes .../apptheme_scrubber_control_pressed_holo.png | Bin 0 -> 1669 bytes .../app/src/main/res/drawable-hdpi/ic_launcher.png | Bin 0 -> 128 bytes .../apptheme_scrubber_control_disabled_holo.png | Bin 0 -> 128 bytes .../apptheme_scrubber_control_focused_holo.png | Bin 0 -> 128 bytes .../apptheme_scrubber_control_normal_holo.png | Bin 0 -> 800 bytes .../apptheme_scrubber_control_pressed_holo.png | Bin 0 -> 947 bytes .../app/src/main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 1980 bytes .../apptheme_scrubber_control_disabled_holo.png | Bin 0 -> 128 bytes .../apptheme_scrubber_control_focused_holo.png | Bin 0 -> 1363 bytes .../apptheme_scrubber_control_normal_holo.png | Bin 0 -> 1593 bytes .../apptheme_scrubber_control_pressed_holo.png | Bin 0 -> 128 bytes .../src/main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 128 bytes .../apptheme_scrubber_control_disabled_holo.png | Bin 0 -> 1415 bytes .../apptheme_scrubber_control_focused_holo.png | Bin 0 -> 1793 bytes .../apptheme_scrubber_control_normal_holo.png | Bin 0 -> 2390 bytes .../apptheme_scrubber_control_pressed_holo.png | Bin 0 -> 2609 bytes .../src/main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 8496 bytes ...ptheme_scrubber_control_selector_holo_light.xml | Bin 0 -> 128 bytes ...eme_scrubber_progress_horizontal_holo_light.xml | 28 ++ AndroidClient/app/src/main/res/layout/main.xml | Bin 0 -> 128 bytes AndroidClient/app/src/main/res/values/strings.xml | 3 + AndroidClient/app/src/main/res/values/styles.xml | 7 + AndroidClient/build.gradle | 19 ++ AndroidClient/gradle.properties | 18 ++ AndroidClient/settings.gradle | 1 + README.md | 1 + dnsserver.cpp | 82 ++++++ dnsserver.h | 65 +++++ portal.cpp | 275 +++++++++++++++++++ portal.h | 24 ++ snowman.ino | 301 +++++++++++++++++++++ util.cpp | 73 +++++ util.h | 12 + weather.cpp | 122 +++++++++ weather.h | 10 + 48 files changed, 1380 insertions(+) create mode 100644 AndroidClient/AmbientNotifications.iml create mode 100644 AndroidClient/README.md create mode 100644 AndroidClient/app/.gitignore create mode 100644 AndroidClient/app/app.iml create mode 100644 AndroidClient/app/build.gradle create mode 100644 AndroidClient/app/proguard-rules.pro create mode 100644 AndroidClient/app/src/main/AndroidManifest.xml create mode 100644 AndroidClient/app/src/main/ic_launcher-web.png create mode 100644 AndroidClient/app/src/main/java/winter/snowman/notifications/NotifListenerService.java create mode 100644 AndroidClient/app/src/main/java/winter/snowman/notifications/WebViewActivity.java create mode 100755 AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_disabled_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_focused_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_normal_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_pressed_holo.png create mode 100644 AndroidClient/app/src/main/res/drawable-hdpi/ic_launcher.png create mode 100755 AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_disabled_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_focused_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_normal_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_pressed_holo.png create mode 100644 AndroidClient/app/src/main/res/drawable-mdpi/ic_launcher.png create mode 100755 AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_disabled_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_focused_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_normal_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_pressed_holo.png create mode 100644 AndroidClient/app/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100755 AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_disabled_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_focused_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_normal_holo.png create mode 100755 AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_pressed_holo.png create mode 100644 AndroidClient/app/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100755 AndroidClient/app/src/main/res/drawable/apptheme_scrubber_control_selector_holo_light.xml create mode 100755 AndroidClient/app/src/main/res/drawable/apptheme_scrubber_progress_horizontal_holo_light.xml create mode 100644 AndroidClient/app/src/main/res/layout/main.xml create mode 100644 AndroidClient/app/src/main/res/values/strings.xml create mode 100644 AndroidClient/app/src/main/res/values/styles.xml create mode 100644 AndroidClient/build.gradle create mode 100644 AndroidClient/gradle.properties create mode 100644 AndroidClient/settings.gradle create mode 100644 README.md create mode 100644 dnsserver.cpp create mode 100644 dnsserver.h create mode 100644 portal.cpp create mode 100644 portal.h create mode 100644 snowman.ino create mode 100644 util.cpp create mode 100644 util.h create mode 100644 weather.cpp create mode 100644 weather.h diff --git a/AndroidClient/AmbientNotifications.iml b/AndroidClient/AmbientNotifications.iml new file mode 100644 index 0000000..2a02201 --- /dev/null +++ b/AndroidClient/AmbientNotifications.iml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/AndroidClient/README.md b/AndroidClient/README.md new file mode 100644 index 0000000..9a64db7 --- /dev/null +++ b/AndroidClient/README.md @@ -0,0 +1 @@ +Based on [AmbientNotifications-Arduino](https://github.com/cle1994/AmbientNotifications) by Christian Le. diff --git a/AndroidClient/app/.gitignore b/AndroidClient/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/AndroidClient/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/AndroidClient/app/app.iml b/AndroidClient/app/app.iml new file mode 100644 index 0000000..4bffb70 --- /dev/null +++ b/AndroidClient/app/app.iml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AndroidClient/app/build.gradle b/AndroidClient/app/build.gradle new file mode 100644 index 0000000..c69adc5 --- /dev/null +++ b/AndroidClient/app/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 21 + buildToolsVersion "21" + + defaultConfig { + applicationId "winter.snowman.notifications" + minSdkVersion 19 + targetSdkVersion 21 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + compile 'com.android.support:appcompat-v7:21.0.2' +} diff --git a/AndroidClient/app/proguard-rules.pro b/AndroidClient/app/proguard-rules.pro new file mode 100644 index 0000000..22d7812 --- /dev/null +++ b/AndroidClient/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/christian/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/AndroidClient/app/src/main/AndroidManifest.xml b/AndroidClient/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a6014da --- /dev/null +++ b/AndroidClient/app/src/main/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AndroidClient/app/src/main/ic_launcher-web.png b/AndroidClient/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000..ff6dc63 Binary files /dev/null and b/AndroidClient/app/src/main/ic_launcher-web.png differ diff --git a/AndroidClient/app/src/main/java/winter/snowman/notifications/NotifListenerService.java b/AndroidClient/app/src/main/java/winter/snowman/notifications/NotifListenerService.java new file mode 100644 index 0000000..dde3d1f --- /dev/null +++ b/AndroidClient/app/src/main/java/winter/snowman/notifications/NotifListenerService.java @@ -0,0 +1,129 @@ +package winter.snowman.notifications; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import android.util.Log; + +import java.net.URL; +import java.io.InputStream; +import java.io.BufferedInputStream; +import java.net.URLConnection; +import java.net.HttpURLConnection; + +/** + * Created by christian on 1/1/15. + */ +public class NotifListenerService extends NotificationListenerService { + private String TAG = this.getClass().getSimpleName(); + + private NotifListenerServiceReceiver notifListenerServiceReceiver; + + @Override + public void onCreate() { + super.onCreate(); + notifListenerServiceReceiver = new NotifListenerServiceReceiver(); + + IntentFilter filter = new IntentFilter(); + filter.addAction("com.cle.ambientnotifications.NOTIFICATION_LISTENER_SERVICE"); + registerReceiver(notifListenerServiceReceiver, filter); + + Log.d(TAG, "NotifListenerService is running"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + unregisterReceiver(notifListenerServiceReceiver); + } + + private void notificationOff() { + Log.i(TAG, "********** notification off"); + URL url; + HttpURLConnection urlConnection; + try { + url = new URL("http://snowman/api?led2=clear"); + urlConnection = (HttpURLConnection) url.openConnection(); + } catch (Exception e) { + e.printStackTrace(); + return; + } + try { + InputStream in = new BufferedInputStream(urlConnection.getInputStream()); + } catch (Exception e) { + e.printStackTrace(); + } finally { + urlConnection.disconnect(); + } + } + + private void notificationOn() { + Log.i(TAG, "********** notification on"); + URL url; + HttpURLConnection urlConnection; + try { + url = new URL("http://snowman/api?led2=blink"); + urlConnection = (HttpURLConnection) url.openConnection(); + } catch (Exception e) { + e.printStackTrace(); + return; + } + try { + InputStream in = new BufferedInputStream(urlConnection.getInputStream()); + } catch (Exception e) { + e.printStackTrace(); + } finally { + urlConnection.disconnect(); + } + } + + @Override + public void onNotificationPosted(StatusBarNotification notif) { + Log.i(TAG, "********** onNotificationPosted"); + Log.i(TAG,"ID :" + notif.getId() + "t" + notif.getNotification().tickerText + + "t" + notif.getPackageName()); + notificationOn(); + } + + @Override + public void onNotificationRemoved(StatusBarNotification notif) { + Log.i(TAG, "********** onNotificationRemoved"); + Log.i(TAG,"ID :" + notif.getId() + "t" + notif.getNotification().tickerText + + "t" + notif.getPackageName()); + notificationOff(); + }; + + class NotifListenerServiceReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getStringExtra("command").equals("clearall")) { + NotifListenerService.this.cancelAllNotifications(); + } else if (intent.getStringExtra("command").equals("list")) { + Log.d(TAG, "command received"); + + Intent i = new Intent("com.cle.ambientnotifications.NOTIFICATION_LISTENER"); + i.putExtra("notification_event", "========="); + sendBroadcast(i); + + int counter = 1; + for (StatusBarNotification notif : NotifListenerService.this.getActiveNotifications()) { + Intent notifIntent = + new Intent("com.cle.ambientnotifications.NOTIFICATION_LISTENER"); + notifIntent.putExtra("notification_event", counter + " " + + notif.getPackageName() + "\n"); + sendBroadcast(notifIntent); + counter += 1; + } + + Intent listIntent = + new Intent("com.cle.ambientnotifications.NOTIFICATION_LISTENER"); + listIntent.putExtra("notification_event", "==== Notification List ===="); + sendBroadcast(listIntent); + } + } + } +} diff --git a/AndroidClient/app/src/main/java/winter/snowman/notifications/WebViewActivity.java b/AndroidClient/app/src/main/java/winter/snowman/notifications/WebViewActivity.java new file mode 100644 index 0000000..0618fd2 --- /dev/null +++ b/AndroidClient/app/src/main/java/winter/snowman/notifications/WebViewActivity.java @@ -0,0 +1,17 @@ +package winter.snowman.notifications; + +import android.app.Activity; +import android.webkit.WebView; +import android.os.Bundle; + +public class WebViewActivity extends Activity { + + private WebView webView; + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + webView = (WebView) findViewById(R.id.webView); + webView.loadUrl("http://snowman"); + } +} diff --git a/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_disabled_holo.png b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_disabled_holo.png new file mode 100755 index 0000000..a1acbb5 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_disabled_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_focused_holo.png b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_focused_holo.png new file mode 100755 index 0000000..1b75394 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_focused_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_normal_holo.png b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_normal_holo.png new file mode 100755 index 0000000..4900d70 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_normal_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_pressed_holo.png b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_pressed_holo.png new file mode 100755 index 0000000..7469c47 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_pressed_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-hdpi/ic_launcher.png b/AndroidClient/app/src/main/res/drawable-hdpi/ic_launcher.png new file mode 100644 index 0000000..00bc047 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_disabled_holo.png b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_disabled_holo.png new file mode 100755 index 0000000..00bc047 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_disabled_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_focused_holo.png b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_focused_holo.png new file mode 100755 index 0000000..00bc047 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_focused_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_normal_holo.png b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_normal_holo.png new file mode 100755 index 0000000..31abfb5 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_normal_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_pressed_holo.png b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_pressed_holo.png new file mode 100755 index 0000000..a296f4c Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_pressed_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-mdpi/ic_launcher.png b/AndroidClient/app/src/main/res/drawable-mdpi/ic_launcher.png new file mode 100644 index 0000000..2067e78 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_disabled_holo.png b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_disabled_holo.png new file mode 100755 index 0000000..00bc047 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_disabled_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_focused_holo.png b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_focused_holo.png new file mode 100755 index 0000000..2f38b65 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_focused_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_normal_holo.png b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_normal_holo.png new file mode 100755 index 0000000..af951cb Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_normal_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_pressed_holo.png b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_pressed_holo.png new file mode 100755 index 0000000..00bc047 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_pressed_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-xhdpi/ic_launcher.png b/AndroidClient/app/src/main/res/drawable-xhdpi/ic_launcher.png new file mode 100644 index 0000000..00bc047 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_disabled_holo.png b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_disabled_holo.png new file mode 100755 index 0000000..a64250e Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_disabled_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_focused_holo.png b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_focused_holo.png new file mode 100755 index 0000000..6a80a0e Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_focused_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_normal_holo.png b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_normal_holo.png new file mode 100755 index 0000000..715ca84 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_normal_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_pressed_holo.png b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_pressed_holo.png new file mode 100755 index 0000000..185e67c Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_pressed_holo.png differ diff --git a/AndroidClient/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/AndroidClient/app/src/main/res/drawable-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..2332309 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/AndroidClient/app/src/main/res/drawable/apptheme_scrubber_control_selector_holo_light.xml b/AndroidClient/app/src/main/res/drawable/apptheme_scrubber_control_selector_holo_light.xml new file mode 100755 index 0000000..00bc047 Binary files /dev/null and b/AndroidClient/app/src/main/res/drawable/apptheme_scrubber_control_selector_holo_light.xml differ diff --git a/AndroidClient/app/src/main/res/drawable/apptheme_scrubber_progress_horizontal_holo_light.xml b/AndroidClient/app/src/main/res/drawable/apptheme_scrubber_progress_horizontal_holo_light.xml new file mode 100755 index 0000000..9535874 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable/apptheme_scrubber_progress_horizontal_holo_light.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + diff --git a/AndroidClient/app/src/main/res/layout/main.xml b/AndroidClient/app/src/main/res/layout/main.xml new file mode 100644 index 0000000..00bc047 Binary files /dev/null and b/AndroidClient/app/src/main/res/layout/main.xml differ diff --git a/AndroidClient/app/src/main/res/values/strings.xml b/AndroidClient/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..0727d11 --- /dev/null +++ b/AndroidClient/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Snowman + diff --git a/AndroidClient/app/src/main/res/values/styles.xml b/AndroidClient/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..69a8ed9 --- /dev/null +++ b/AndroidClient/app/src/main/res/values/styles.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/AndroidClient/build.gradle b/AndroidClient/build.gradle new file mode 100644 index 0000000..234dff0 --- /dev/null +++ b/AndroidClient/build.gradle @@ -0,0 +1,19 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.0.0-rc4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} diff --git a/AndroidClient/gradle.properties b/AndroidClient/gradle.properties new file mode 100644 index 0000000..1d3591c --- /dev/null +++ b/AndroidClient/gradle.properties @@ -0,0 +1,18 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true \ No newline at end of file diff --git a/AndroidClient/settings.gradle b/AndroidClient/settings.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/AndroidClient/settings.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa44a6a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Based on [esp8266-wifi-setup](https://github.com/9SQ/esp8266-wifi-setup/) by Kei Yoshimura. diff --git a/dnsserver.cpp b/dnsserver.cpp new file mode 100644 index 0000000..9fe1925 --- /dev/null +++ b/dnsserver.cpp @@ -0,0 +1,82 @@ +#include "dnsserver.h" + +DNSServer::DNSServer() +{ + _ttl = htonl(600); + _errorReplyCode = DNSReplyCode::NonExistentDomain; +} + +bool DNSServer::start(const IPAddress &resolvedIP) +{ + _buffer = NULL; + _resolvedIP[0] = resolvedIP[0]; + _resolvedIP[1] = resolvedIP[1]; + _resolvedIP[2] = resolvedIP[2]; + _resolvedIP[3] = resolvedIP[3]; + return _udp.begin(53) == 1; +} + +void DNSServer::stop() +{ + _udp.stop(); + free(_buffer); + _buffer = NULL; +} + +void DNSServer::processNextRequest() +{ + _currentPacketSize = _udp.parsePacket(); + if (_currentPacketSize) + { + if (_buffer != NULL) free(_buffer); + _buffer = (unsigned char*)malloc(_currentPacketSize * sizeof(char)); + if (_buffer == NULL) return; + _udp.read(_buffer, _currentPacketSize); + + _dnsHeader = (DNSHeader*) _buffer; + + if (_dnsHeader->QR == DNS_QR_QUERY && + _dnsHeader->OPCode == DNS_OPCODE_QUERY && + requestIncludesOnlyOneQuestion()) + { + replyWithIP(); // reply with the same ip regardless of request + } + } +} + +bool DNSServer::requestIncludesOnlyOneQuestion() +{ + return ntohs(_dnsHeader->QDCount) == 1 && + _dnsHeader->ANCount == 0 && + _dnsHeader->NSCount == 0 && + _dnsHeader->ARCount == 0; +} + +void DNSServer::replyWithIP() +{ + if (_buffer == NULL) return; + _dnsHeader->QR = DNS_QR_RESPONSE; + _dnsHeader->ANCount = _dnsHeader->QDCount; + _dnsHeader->QDCount = _dnsHeader->QDCount; + //_dnsHeader->RA = 1; + + _udp.beginPacket(_udp.remoteIP(), _udp.remotePort()); + _udp.write(_buffer, _currentPacketSize); + + _udp.write((uint8_t)192); // answer name is a pointer + _udp.write((uint8_t)12); // pointer to offset at 0x00c + + _udp.write((uint8_t)0); // 0x0001 answer is type A query (host address) + _udp.write((uint8_t)1); + + _udp.write((uint8_t)0); //0x0001 answer is class IN (internet address) + _udp.write((uint8_t)1); + + _udp.write((unsigned char*)&_ttl, 4); + + // Length of RData is 4 bytes (because, in this case, RData is IPv4) + _udp.write((uint8_t)0); + _udp.write((uint8_t)4); + _udp.write(_resolvedIP, sizeof(_resolvedIP)); + _udp.endPacket(); +} diff --git a/dnsserver.h b/dnsserver.h new file mode 100644 index 0000000..6a6c518 --- /dev/null +++ b/dnsserver.h @@ -0,0 +1,65 @@ +#include +#include + +#ifndef __DNSSERVER_H__ +#define __DNSSERVER_H__ + +#define DNS_QR_QUERY 0 +#define DNS_QR_RESPONSE 1 +#define DNS_OPCODE_QUERY 0 + +enum class DNSReplyCode +{ + NoError = 0, + FormError = 1, + ServerFailure = 2, + NonExistentDomain = 3, + NotImplemented = 4, + Refused = 5, + YXDomain = 6, + YXRRSet = 7, + NXRRSet = 8 +}; + +struct DNSHeader +{ + uint16_t ID; // identification number + unsigned char RD : 1; // recursion desired + unsigned char TC : 1; // truncated message + unsigned char AA : 1; // authoritive answer + unsigned char OPCode : 4; // message_type + unsigned char QR : 1; // query/response flag + unsigned char RCode : 4; // response code + unsigned char Z : 3; // its z! reserved + unsigned char RA : 1; // recursion available + uint16_t QDCount; // number of question entries + uint16_t ANCount; // number of answer entries + uint16_t NSCount; // number of authority entries + uint16_t ARCount; // number of resource entries +}; + +class DNSServer +{ + public: + DNSServer(); + void processNextRequest(); + bool start(const IPAddress &resolvedIP); + void stop(); + + private: + WiFiUDP _udp; + uint16_t _port; + String _domainName; + unsigned char _resolvedIP[4]; + int _currentPacketSize; + unsigned char* _buffer; + DNSHeader* _dnsHeader; + uint32_t _ttl; + DNSReplyCode _errorReplyCode; + + bool requestIncludesOnlyOneQuestion(); + void replyWithIP(); +}; + +#endif + diff --git a/portal.cpp b/portal.cpp new file mode 100644 index 0000000..5013249 --- /dev/null +++ b/portal.cpp @@ -0,0 +1,275 @@ +#include "portal.h" + +//#define DEBUG + +#define NONE -1 +#define BLINK -2 +#define FADEIN -3 +#define FADEOUT -4 + +const IPAddress apIP(192, 168, 1, 1); +const char* apSSID; + +boolean isAPactive; +String scanSSIDList; + +int apiOverrides[4] = {NONE, NONE, NONE, NONE}; +boolean requestLedUpdate = false; + +DNSServer dnsServer; +ESP8266WebServer webServer(80); + +#define ICONPATH "http://openweathermap.org/img/w/" +String weatherIcon = ""; + +void portalSetup(String ssid) { + apSSID = ssid.c_str(); + + WiFi.hostname(ssid); + // connect to configured network or fallback to setup AP + if (!enterClientMode(NULL, NULL)) { + enterAPMode(); // let the user enter the credentials + } + startWebServer(); +} + +void portalLoop() { + if (isAPactive) { + dnsServer.processNextRequest(); + } + webServer.handleClient(); +} + +boolean isConnected() { + return !isAPactive; +} + +void startWebServer() { +#ifdef DEBUG + Serial.print(F("Starting Web Server at ")); + Serial.print(WiFi.softAPIP()); Serial.print(" "); Serial.println(WiFi.localIP()); +#endif + + webServer.on("/settings", []() { + String s = "

WLAN-Einstellungen

Bitte wähle deinen Accesspoint und gib das Passwort ein.

"; + s += "

Passwort:
"; + webServer.send(200, "text/html", makePage("WLAN-Einstellungen", s)); + }); + + webServer.on("/setap", []() { + String ssid = urlDecode(webServer.arg("ssid")); +#ifdef DEBUG + Serial.print(F("Connecting to: ")); Serial.println(ssid); +#endif + String pass = urlDecode(webServer.arg("pass")); + + String s = "

Einrichtung abgeschlossen.

Das Gerät wird zu \""; + s += ssid; + s += "\" verbunden."; + webServer.send(200, "text/html", makePage("WLAN-Einstellungen", s)); + + if (!enterClientMode(ssid.c_str(), pass.c_str())) { + enterAPMode(); // wrong passphrase + } + }); + + webServer.on("/reset", []() { + String s = "

Zurückgesetzt

Zum Einstellen bitte mit dem Accesspoint \"Snowman\" verbinden.

"; + webServer.send(200, "text/html", makePage("Zurücksetzen", s)); + enterAPMode(); + }); + + webServer.on("/api", []() { + String response = ""; + + for (int n = 0; n < webServer.args(); n++) { + String sArgName = webServer.argName(n); + + if (sArgName.startsWith("led")) { + String sPinNumber = sArgName.substring(3); + if (sPinNumber == "") { + String msg = "API: Invalid argument '" + sArgName + "' - pin number invalid\n"; + response += msg; +#ifdef DEBUG + Serial.print(msg); +#endif + continue; + } + int pinNumber = sPinNumber.toInt(); + if (pinNumber < 0 || pinNumber > 3) { + String msg = "API: Invalid argument '" + sArgName + "' - pin number out of range\n"; + response += msg; +#ifdef DEBUG + Serial.print(msg); +#endif + continue; + } + + String sArg = webServer.arg(n); + int val = -1; + if (sArg == "fade" || sArg == "fadein") { + val = FADEIN; + } else if (sArg == "fadeout") { + val = FADEOUT; + } else if (sArg == "blink") { + val = BLINK; + } else if (sArg == "clear") { + val = NONE; + } else { + val = sArg.toInt(); + } + + if ((val != FADEIN && val != FADEOUT && val != BLINK && val != NONE && val < 0) || val > PWMRANGE) { + String msg = "API: Value out of range for argument '" + sArgName + "': '" + String(val) + "'\n"; + response += msg; +#ifdef DEBUG + Serial.print(msg); +#endif + continue; + } + String msg = "API: Overriding pin " + String(pinNumber) + " to " + String(val) + "\n"; + response += msg; +#ifdef DEBUG + Serial.print(msg); +#endif + apiOverrides[pinNumber] = val; + requestLedUpdate = true; + } + if (webServer.argName(n) == "clear") { + if (webServer.arg(n) == "true") { + for (int j = 0; j < 4; j++) { + apiOverrides[j] = NONE; + } + } + String msg = "API: Cleared pin overrides\n"; + response += msg; +#ifdef DEBUG + Serial.print(msg); +#endif + requestLedUpdate = true; + } + } + + webServer.send(200, "text/html", makePage("API", "
" + response + "
")); + }); + + webServer.on("/", []() { + String s = "

Snowman

Einstellungen zurücksetzen

"; + s += "

Einstellungen ändern

"; + s += "

"; + webServer.send(200, "text/html", makePage("Snowman", s)); + }); + + webServer.onNotFound([]() { + String redirect = "http://" + ipToStr(apIP) + "/settings"; + webServer.send(200, "text/html", makeRedirectPage(redirect, 2)); + }); + + webServer.begin(); +} + +int getAPIOverride(int pin) { + return apiOverrides[pin]; +} + +boolean apiNeedLedUpdate() { + return requestLedUpdate; +} + +void apiUpdatedLeds() { + requestLedUpdate = false; +} + +void setWeatherIcon(String id) { + weatherIcon = id; +} + +// must enter AP mode after scanning (SSID & PW get lost) +void scanSSIDs() { +#ifdef DEBUG + Serial.println(F("Scanning networks")); +#endif + + WiFi.disconnect(); + WiFi.mode(WIFI_STA); + delay(100); + + int n = WiFi.scanNetworks(); + delay(100); + scanSSIDList = ""; + for (int i = 0; i < n; ++i) { + scanSSIDList += ""; + } + +#ifdef DEBUG + Serial.println(F("Finished scanning")); +#endif +} + + +// also read https://github.com/esp8266/Arduino/issues/529 + +// sets up an AP for the user to enter his/her WiFi credentials +void enterAPMode() { +#ifdef DEBUG + Serial.println(F("Entering AP mode")); +#endif + + scanSSIDs(); + + // create ad hoc setup-network + WiFi.disconnect(); + WiFi.mode(WIFI_AP); + isAPactive = true; + delay(100); + WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0)); + WiFi.softAP(apSSID); +#ifdef DEBUG + Serial.println(F("Started Access Point")); +#endif + + dnsServer.start(apIP); +} + +// returns true if successfully connected to the last AP +boolean enterClientMode(const char* ssid, const char *passphrase) { + dnsServer.stop(); + + if (ssid && strlen(ssid) > 0) { // if ssid is not given, try to connect to the memorized AP + WiFi.disconnect(); + WiFi.begin(ssid, passphrase); // register credentials + } + WiFi.mode(WIFI_STA); + isAPactive = false; + delay(100); + + // SSID and password are memorized until WiFi.begin() + // https://github.com/esp8266/Arduino/issues/244 + int count = 0; +#ifdef DEBUG + Serial.print(F("Waiting for Wi-Fi connection to ")); Serial.println(WiFi.SSID()); +#endif + while (count++ < 30) { // attempt for about 30 * 1/2 = 15 seconds + if (WiFi.status() == WL_CONNECTED) { +#ifdef DEBUG + Serial.println(); Serial.println(F("Connected!")); +#endif + return true; + } + delay(500); +#ifdef DEBUG + Serial.print("."); +#endif + } +#ifdef DEBUG + Serial.println(); Serial.println(F("Not connected")); +#endif + return false; +} + diff --git a/portal.h b/portal.h new file mode 100644 index 0000000..1c5cb6e --- /dev/null +++ b/portal.h @@ -0,0 +1,24 @@ +#include +#include + +#include "dnsserver.h" +#include "util.h" + +#ifndef __PORTAL_H__ +#define __PORTAL_H__ + +void portalSetup(String ssid); +void portalLoop(); +boolean isConnected(); +void startWebServer(); +void setWeatherIcon(String id); +int getAPIOverride(int pinId); +boolean apiNeedLedUpdate(); +void apiUpdatedLeds(); + +void scanSSIDs(); +void enterAPMode(); +boolean enterClientMode(const char* ssid, const char *passphrase); + +#endif + diff --git a/snowman.ino b/snowman.ino new file mode 100644 index 0000000..963ad5e --- /dev/null +++ b/snowman.ino @@ -0,0 +1,301 @@ +#include +#include +#include +#include +#include +#include + +// TODO: needs more OOP + +//#define DEBUG +//#define TESTWEATHER + +#include "portal.h" +#include "util.h" +#include "weather.h" + +#define SNOWMANSSID "Snowman" + +#if defined(DEBUG) || defined(TESTWEATHER) +#define UPDATEFREQ 30 // s while testing +#else +#define UPDATEFREQ 60*60 // s => hourly +#endif + +#define LEDFREQ 0.25 // s +#define FADESTEP PWMRANGE / (5 / LEDFREQ) +#define BLINKTHRESHOLD 1 / LEDFREQ +#define BLINKDARKMULTIPLIER 2 // 2 * longer dark than light + +/* + * Layout: + * GPIO0: red LED [bottom] + * TX: blue builtin LED [bottom] + * GPIO2: green LED [bottom] | NOT USED YET + * RX: blue LED [top] + */ + +#define GPIO0 0 +#define TX 1 +#define GPIO2 2 +#define RX 3 + +#define LRED GPIO0 +#define LGREEN GPIO2 +#define LBLUET RX +#define LBLUEB TX // builtin + +// when changing this, c&p to portal.cpp +// TODO use enums instead of defines? +#define OFF 0 +#define ON 1 +#define NONE -1 +#define BLINK -2 +#define FADEIN -3 +#define FADEOUT -4 + +int states[4] = {OFF, OFF, OFF, OFF}; +int values[4] = {0, 0, 0, 0}; +int overrides[4] = {NONE, NONE, NONE, NONE}; + +Ticker updaterData; +Ticker updaterLed; +boolean needsUpdate = true; +boolean needUpdateLeds = false; + +void setup() { +#ifdef DEBUG + Serial.begin(115200); +#endif + portalSetup(SNOWMANSSID); + updaterData.attach(UPDATEFREQ, toggleUpdaterFlag); + updaterLed.attach(LEDFREQ, toggleLedFlag); + ArduinoOTA.setHostname(SNOWMANSSID); + ArduinoOTA.begin(); +#ifndef DEBUG + pinMode(TX, OUTPUT); +#endif + pinMode(RX, OUTPUT); + pinMode(GPIO0, OUTPUT); + pinMode(GPIO2, OUTPUT); + states[LGREEN] = BLINK; + resetLeds(); +} + +void toggleUpdaterFlag() { +#ifdef DEBUG + Serial.println(F("Scheduled update.")); +#endif + needsUpdate = true; +} + +void toggleLedFlag() { + needUpdateLeds = true; +} + +void ledWrite(int pin, int state) { + // TX has high-low reversed + if (pin == TX) { +#ifdef DEBUG + return; +#endif + analogWrite(pin, PWMRANGE - state); + } else { + analogWrite(pin, state); + } +} + +#define THROTTLE_SECS 30 +unsigned int throttleWait = 5000; // wait 5s after bootup + +void loop() { + portalLoop(); + ArduinoOTA.handle(); + + if (throttleWait > 0) { + // do not send more than 1 request per 30s + throttleWait--; + } + + if (needsUpdate && throttleWait <= 0) { + if (isConnected()) { + needsUpdate = false; + String statusCode = getWeatherStatus(); + throttleWait = THROTTLE_SECS * 1000; + if (statusCode == "-1") { + needsUpdate = true; +#ifdef DEBUG + Serial.println(F("Did not get weather data.")); +#endif + } else { + interpretWeatherCode(statusCode); + } + } + } + + if (apiNeedLedUpdate()) { + for (int n = 0; n < 4; n++) { + // TODO create a getAPIOverrides() function that returns an array + // TODO overriding has a delay + overrides[n] = getAPIOverride(n); + } + resetLeds(); + apiUpdatedLeds(); + } + + // updateLeds: regularely + // values[] holds a timer counter (BLINK) or the current brightness (other) + if (needUpdateLeds) { + updateLeds(); + needUpdateLeds = false; + } + delay(1); +} + +void updateLeds() { + for (int n = 0; n < 4; n++) { + if (overrides[n] == NONE) { + ledWrite(n, ledUpdate(states[n], values[n])); + } else if (overrides[n] < 0) { // update effects + ledWrite(n, ledUpdate(overrides[n], values[n])); + } + } +} + +// resetLeds: used when states[] was changed, resets values[] +void resetLeds() { + for (int n = 0; n < 4; n++) { + if (overrides[n] == NONE) { + ledWrite(n, ledInitValue(states[n], values[n])); + } else { + ledWrite(n, ledInitValue(overrides[n], values[n])); + } + } +} + +int ledInitValue(int state, int &value) { + if (state == OFF || state == BLINK || state == FADEIN) { + value = 0; + } + if (state == ON || state == FADEOUT) { + value = PWMRANGE; + } + return value; +} + +// takes and modifies LED state, value; returns new brightness +int ledUpdate(int &state, int &value) { + if (state == BLINK) { + // value < 0: off; value >= 0: on + value++; + if (value >= BLINKTHRESHOLD) { // turn off thresh*freq s + value = -BLINKTHRESHOLD * BLINKDARKMULTIPLIER; // "overflow" and wait until turnon + return 0; + } + if (value == 0) { // turn on again + return PWMRANGE; + } + } + // fade in until max brightness + if (state == FADEIN) { + value += FADESTEP; + if (value > PWMRANGE) { + value = PWMRANGE; + state = FADEOUT; + } + return value; + } + // …and then fade out again + if (state == FADEOUT) { + value -= FADESTEP; + if (value < 0) { + value = 0; + state = FADEIN; + } + return value; + } + return value; +} + +void interpretWeatherCode(String code) { +#ifdef TESTWEATHER + switch (random(5)) { + case 0: code = "01"; break; + case 1: code = "09"; break; + case 2: code = "11"; break; + case 3: code = "13"; break; + case 4: code = "50"; break; + } + code += (random(1) == 0 ? "d" : "n"); +#ifdef DEBUG + Serial.println(F("Faking weather data:")); +#endif +#endif + + // http://openweathermap.org/weather-conditions + boolean isDay = (code.substring(2) == "d"); + int id = code.substring(0, 2).toInt(); + +#ifdef DEBUG + if (isDay) { + Serial.print(F("day ")); + } else { + Serial.print(F("night ")); + } + Serial.print(F("icon: ")); + Serial.println(id); +#endif + + states[LGREEN] = OFF; + + // top LEDs: daytime + // bottom LEDs: rain/snow/… + switch (id) { + case 1: + case 2: + case 3: + case 4: + if (isDay) { + states[LBLUET] = OFF; + states[LRED] = ON; + } else { + states[LBLUET] = ON; + states[LRED] = OFF; + } + states[LBLUEB] = OFF; + break; + case 9: + case 10: + if (isDay) { + states[LBLUET] = OFF; + states[LRED] = ON; + } else { + states[LBLUET] = ON; + states[LRED] = OFF; + } + states[LBLUEB] = FADEIN; + break; + case 11: + if (isDay) { + states[LBLUET] = OFF; + states[LRED] = ON; + } else { + states[LBLUET] = ON; + states[LRED] = OFF; + } + states[LBLUEB] = BLINK; + break; + case 13: + states[LBLUET] = FADEIN; + states[LRED] = OFF; + states[LBLUEB] = FADEOUT; + break; + case 50: + states[LBLUET] = OFF; + states[LRED] = OFF; + states[LBLUEB] = FADEIN; + break; + } + resetLeds(); + setWeatherIcon(code); +} diff --git a/util.cpp b/util.cpp new file mode 100644 index 0000000..bdd1a51 --- /dev/null +++ b/util.cpp @@ -0,0 +1,73 @@ +#include "util.h" + +// creates a page that redirects the user to 'target' after 'timeout' +String makeRedirectPage(String target, int timeout) { + String s = ""; + s += ""; + s += ""; + s += ""; + s += "Weiterleitung…"; + s += "

Leite nach " + target + " weiter…

"; + s += ""; + return s; +} + +// returns html skeleton filled with 'title' and 'contents' +String makePage(String title, String contents) { + String s = ""; + s += ""; + s += ""; + s += ""; + s += title; + s += ""; + s += contents; + s += ""; + return s; +} + +String urlDecode(String input) { + String s = input; + s.replace("%20", " "); + s.replace("+", " "); + s.replace("%21", "!"); + s.replace("%22", "\""); + s.replace("%23", "#"); + s.replace("%24", "$"); + s.replace("%25", "%"); + s.replace("%26", "&"); + s.replace("%27", "\'"); + s.replace("%28", "("); + s.replace("%29", ")"); + s.replace("%30", "*"); + s.replace("%31", "+"); + s.replace("%2C", ","); + s.replace("%2E", "."); + s.replace("%2F", "/"); + s.replace("%2C", ","); + s.replace("%3A", ":"); + s.replace("%3A", ";"); + s.replace("%3C", "<"); + s.replace("%3D", "="); + s.replace("%3E", ">"); + s.replace("%3F", "?"); + s.replace("%40", "@"); + s.replace("%5B", "["); + s.replace("%5C", "\\"); + s.replace("%5D", "]"); + s.replace("%5E", "^"); + s.replace("%5F", "-"); + s.replace("%60", "`"); + return s; +} + +// converts IPv4 array to String +String ipToStr(IPAddress address) { + String res = ""; + for (int i = 0; i < 3; i++) { + res += address[i]; + res += "."; + } + res += String(address[3]); + return res; +} + diff --git a/util.h b/util.h new file mode 100644 index 0000000..4d8df4d --- /dev/null +++ b/util.h @@ -0,0 +1,12 @@ +#include + +#ifndef __UTIL_H__ +#define __UTIL_H__ + +String makeRedirectPage(String target, int timeout); +String makePage(String title, String contents); +String urlDecode(String input); +String ipToStr(IPAddress address); + +#endif + diff --git a/weather.cpp b/weather.cpp new file mode 100644 index 0000000..4fabb1e --- /dev/null +++ b/weather.cpp @@ -0,0 +1,122 @@ +#include "weather.h" + +//#define DEBUG + +#define HTTP_TIMEOUT 5000 // ms +#define DOWNLOADSIZE 1100 // http buffer size, *should* be enough for a cnt=2 request +#define JSONSIZE 1500 // json buffer size, for a cnt=2 request +// ^ be careful with counter over/underflows! + +#define CITYID "YOURCITYID" +#define APIKEY "YOURAPIKEY" + +#define APIHOST "api.openweathermap.org" +#define APIPATH "/data/2.5/forecast?id=" CITYID "&appid=" APIKEY "&cnt=2" +// "cnt=2" for 3 hour forecast + +// queries OWM for the 3-hour-forecast, returns icon code +// see http://openweathermap.org/weather-conditions for codes +String getWeatherStatus() { +#ifdef DEBUG + Serial.println("Requesting weather data."); +#endif + + WiFiClient cli; + cli.connect(APIHOST, 80); + cli.println("GET " APIPATH " HTTP/1.1"); + cli.println("Host: " APIHOST); + cli.println("Connection: close\r\n"); + +#ifdef DEBUG + Serial.println("Sent request: " APIHOST APIPATH); +#endif + + unsigned int timeoutTimer = HTTP_TIMEOUT; + boolean gotHeaders = false; + + // skip headers + while (cli.connected() && timeoutTimer-- && !gotHeaders) { + while (cli.available()) { + String line = cli.readStringUntil('\n'); + if (line == "\r") { + gotHeaders = true; + break; + } + } + delay(1); + } + if (!gotHeaders) { +#ifdef DEBUG + Serial.println("Did not receive headers."); +#endif + cli.stop(); + if (!cli.connected()) { +#ifdef DEBUG + Serial.println("Connection was reset."); +#endif + } + return "-1"; + } + + // read response into char array + char response[DOWNLOADSIZE]; + unsigned int buffPointer = 0; + boolean jsonStarted = false; + int charsSinceJsonEnd = 0; + while (cli.connected() && timeoutTimer-- && buffPointer < DOWNLOADSIZE) { + while (cli.available() && buffPointer < DOWNLOADSIZE) { + char ch = (char) cli.read(); + if (!jsonStarted) { + if (ch == '{') { + jsonStarted = true; + } + } + if (jsonStarted) { + if (ch == '}') { + charsSinceJsonEnd = 0; + } else { + charsSinceJsonEnd++; + } + response[buffPointer++] = ch; + } + } + delay(1); + } + cli.stop(); + response[buffPointer - charsSinceJsonEnd] = '\0'; // OWM seems to send some weird extra characters we do not want + + if (!timeoutTimer) { +#ifdef DEBUG + Serial.println("Timed out."); +#endif + return "-1"; + } + if (buffPointer == DOWNLOADSIZE) { +#ifdef DEBUG + Serial.println("Maximum buffer size exceeded."); +#endif + return "-1"; + } + +#ifdef DEBUG + Serial.println("'" + String(response) + "'"); +#endif + + StaticJsonBuffer json; + JsonObject& root = json.parseObject(response); + if (!root.success()) { +#ifdef DEBUG + Serial.println("JSON invalid."); +#endif + return "-1"; + } + String strConditionId = root["list"][1]["weather"][0]["icon"].asString(); + if (!strConditionId.length()) { + return "-1"; + } +#ifdef DEBUG + Serial.println("Parsed JSON. Weather code: " + strConditionId); +#endif + return strConditionId; // 3 hour future weather by icon code +} + diff --git a/weather.h b/weather.h new file mode 100644 index 0000000..54f497e --- /dev/null +++ b/weather.h @@ -0,0 +1,10 @@ +#include +#include + +#ifndef __WEATHER_H__ +#define __WEATHER_H__ + +String getWeatherStatus(); + +#endif + -- cgit v1.3.1