diff options
48 files changed, 1380 insertions, 0 deletions
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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" type="JAVA_MODULE" version="4"> + <component name="FacetManager"> + <facet type="java-gradle" name="Java-Gradle"> + <configuration> + <option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" /> + </configuration> + </facet> + </component> + <component name="NewModuleRootManager" inherit-compiler-output="false"> + <output url="file://$MODULE_DIR$/build/classes/main" /> + <output-test url="file://$MODULE_DIR$/build/classes/test" /> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <excludeFolder url="file://$MODULE_DIR$/.gradle" /> + </content> + <orderEntry type="inheritedJdk" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> + 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" type="JAVA_MODULE" version="4"> + <component name="FacetManager"> + <facet type="android-gradle" name="Android-Gradle"> + <configuration> + <option name="GRADLE_PROJECT_PATH" value=":app" /> + </configuration> + </facet> + <facet type="android" name="Android"> + <configuration> + <option name="SELECTED_BUILD_VARIANT" value="debug" /> + <option name="ASSEMBLE_TASK_NAME" value="assembleDebug" /> + <option name="COMPILE_JAVA_TASK_NAME" value="compileDebugSources" /> + <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleDebugTest" /> + <option name="SOURCE_GEN_TASK_NAME" value="generateDebugSources" /> + <option name="TEST_SOURCE_GEN_TASK_NAME" value="generateDebugTestSources" /> + <option name="ALLOW_USER_CONFIGURATION" value="false" /> + <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" /> + <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" /> + <option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" /> + <option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" /> + </configuration> + </facet> + </component> + <component name="NewModuleRootManager" inherit-compiler-output="false"> + <output url="file://$MODULE_DIR$/build/intermediates/classes/debug" /> + <exclude-output /> + <content url="file://$MODULE_DIR$"> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/debug" isTestSource="false" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/debug" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/debug" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/debug" isTestSource="true" generated="true" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/debug" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/debug" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/jni" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" /> + <sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" /> + <excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" /> + <excludeFolder url="file://$MODULE_DIR$/build/outputs" /> + <excludeFolder url="file://$MODULE_DIR$/build/tmp" /> + </content> + <orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" /> + <orderEntry type="sourceFolder" forTests="false" /> + <orderEntry type="library" exported="" name="support-annotations-21.0.2" level="project" /> + <orderEntry type="library" exported="" name="support-v4-21.0.2" level="project" /> + <orderEntry type="library" exported="" name="appcompat-v7-21.0.2" level="project" /> + </component> +</module> + 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 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="winter.snowman.notifications"> + + <uses-permission android:name="android.permission.INTERNET"/> + + <application android:allowBackup="true" + android:label="@string/app_name" + android:icon="@drawable/ic_launcher" + android:theme="@style/AppTheme"> + + <activity android:name=".WebViewActivity" + android:label="@string/app_name" + android:screenOrientation="portrait" + android:configChanges="orientation|keyboardHidden" > + + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + + </activity> + + <service android:name=".NotifListenerService" + android:label="@string/app_name" + android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" > + <intent-filter> + <action android:name="android.service.notification.NotificationListenerService" /> + </intent-filter> + + </service> + + </application> + +</manifest> diff --git a/AndroidClient/app/src/main/ic_launcher-web.png b/AndroidClient/app/src/main/ic_launcher-web.png Binary files differnew file mode 100644 index 0000000..ff6dc63 --- /dev/null +++ b/AndroidClient/app/src/main/ic_launcher-web.png 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 Binary files differnew file mode 100755 index 0000000..a1acbb5 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_disabled_holo.png 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 Binary files differnew file mode 100755 index 0000000..1b75394 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_focused_holo.png 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 Binary files differnew file mode 100755 index 0000000..4900d70 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_normal_holo.png 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 Binary files differnew file mode 100755 index 0000000..7469c47 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-hdpi/apptheme_scrubber_control_pressed_holo.png diff --git a/AndroidClient/app/src/main/res/drawable-hdpi/ic_launcher.png b/AndroidClient/app/src/main/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..00bc047 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-hdpi/ic_launcher.png 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 Binary files differnew file mode 100755 index 0000000..00bc047 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_disabled_holo.png 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 Binary files differnew file mode 100755 index 0000000..00bc047 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_focused_holo.png 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 Binary files differnew file mode 100755 index 0000000..31abfb5 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_normal_holo.png 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 Binary files differnew file mode 100755 index 0000000..a296f4c --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-mdpi/apptheme_scrubber_control_pressed_holo.png diff --git a/AndroidClient/app/src/main/res/drawable-mdpi/ic_launcher.png b/AndroidClient/app/src/main/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..2067e78 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-mdpi/ic_launcher.png 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 Binary files differnew file mode 100755 index 0000000..00bc047 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_disabled_holo.png 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 Binary files differnew file mode 100755 index 0000000..2f38b65 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_focused_holo.png 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 Binary files differnew file mode 100755 index 0000000..af951cb --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_normal_holo.png 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 Binary files differnew file mode 100755 index 0000000..00bc047 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-xhdpi/apptheme_scrubber_control_pressed_holo.png diff --git a/AndroidClient/app/src/main/res/drawable-xhdpi/ic_launcher.png b/AndroidClient/app/src/main/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..00bc047 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100755 index 0000000..a64250e --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_disabled_holo.png 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 Binary files differnew file mode 100755 index 0000000..6a80a0e --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_focused_holo.png 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 Binary files differnew file mode 100755 index 0000000..715ca84 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_normal_holo.png 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 Binary files differnew file mode 100755 index 0000000..185e67c --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-xxhdpi/apptheme_scrubber_control_pressed_holo.png diff --git a/AndroidClient/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/AndroidClient/app/src/main/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..2332309 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable-xxhdpi/ic_launcher.png 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 Binary files differnew file mode 100755 index 0000000..00bc047 --- /dev/null +++ b/AndroidClient/app/src/main/res/drawable/apptheme_scrubber_control_selector_holo_light.xml 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2010 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@android:id/background" + android:drawable="@drawable/apptheme_scrubber_track_holo_light" /> + <item android:id="@android:id/secondaryProgress"> + <scale android:scaleWidth="100%" + android:drawable="@drawable/apptheme_scrubber_secondary_holo" /> + </item> + <item android:id="@android:id/progress"> + <scale android:scaleWidth="100%" + android:drawable="@drawable/apptheme_scrubber_primary_holo" /> + </item> +</layer-list> diff --git a/AndroidClient/app/src/main/res/layout/main.xml b/AndroidClient/app/src/main/res/layout/main.xml Binary files differnew file mode 100644 index 0000000..00bc047 --- /dev/null +++ b/AndroidClient/app/src/main/res/layout/main.xml 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 @@ +<resources> + <string name="app_name">Snowman</string> +</resources> 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 @@ +<resources> + + <!-- Base application theme. --> + <style name="AppTheme" parent="@android:style/Theme.Holo.Light"> + </style> + +</resources> 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 <WiFiUdp.h> +#include <lwip/def.h> + +#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 = "<h1>WLAN-Einstellungen</h1><p>Bitte wähle deinen Accesspoint und gib das Passwort ein.</p>"; + s += "<form method=\"get\" action=\"setap\"><label>SSID: </label><select name=\"ssid\">"; + s += scanSSIDList; + s += "</select><br>Passwort: <input name=\"pass\" length=64 type=\"password\"><input type=\"submit\"></form>"; + 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 = "<h1>Einrichtung abgeschlossen.</h1><p>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 = "<h1>Zurückgesetzt</h1><p>Zum Einstellen bitte mit dem Accesspoint \"Snowman\" verbinden.</p>"; + 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", "<pre>" + response + "</pre>")); + }); + + webServer.on("/", []() { + String s = "<h1>Snowman</h1><p><a href=\"/reset\">Einstellungen zurücksetzen</a></p>"; + s += "<p><a href=\"/settings\">Einstellungen ändern</a></p>"; + s += "<p><img src=\"" ICONPATH + weatherIcon + ".png\" /></p>"; + 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 += "<option value=\""; + scanSSIDList += WiFi.SSID(i); // FIXME XSS attack possible? + scanSSIDList += "\">"; + scanSSIDList += WiFi.SSID(i); + scanSSIDList += "</option>"; + } + +#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 <ESP8266WiFi.h> +#include <ESP8266WebServer.h> + +#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 <ESP8266WiFi.h> +#include <ESP8266WebServer.h> +#include <Ticker.h> +#include <ArduinoJson.h> +#include <ESP8266mDNS.h> +#include <ArduinoOTA.h> + +// 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 = "<!DOCTYPE html><html><head>"; + s += "<meta name=\"viewport\" content=\"width=device-width,user-scalable=0\">"; + s += "<meta charset=\"utf-8\">"; + s += "<meta http-equiv=\"refresh\" content=\"" + String(timeout) + ";url=" + target + "\">"; + s += "<title>Weiterleitung…</title></head><body>"; + s += "<p>Leite nach <a href=\"" + target + "\">" + target + "</a> weiter…</p>"; + s += "</body></html>"; + return s; +} + +// returns html skeleton filled with 'title' and 'contents' +String makePage(String title, String contents) { + String s = "<!DOCTYPE html><html><head>"; + s += "<meta name=\"viewport\" content=\"width=device-width,user-scalable=0\">"; + s += "<meta charset=\"utf-8\">"; + s += "<title>"; + s += title; + s += "</title></head><body>"; + s += contents; + s += "</body></html>"; + 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; +} + @@ -0,0 +1,12 @@ +#include <ESP8266WiFi.h> + +#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<JSONSIZE> 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 <WiFiClient.h> +#include <ArduinoJson.h> + +#ifndef __WEATHER_H__ +#define __WEATHER_H__ + +String getWeatherStatus(); + +#endif + |
