From 3fe6e980c2b0a469d5f8fd1a1eaa656191d4c09c Mon Sep 17 00:00:00 2001 From: fauresystems <34378695+fauresystems@users.noreply.github.com> Date: Sun, 3 Nov 2019 09:34:43 +0100 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 349 +++++++++++++++ ArduinoProps.sln | 19 + ArduinoProps.vcxitems | 56 +++ ArduinoProps.vcxitems.filters | 92 ++++ EXAMPLES.md | 409 ++++++++++++++++++ LICENSE | 21 + LICENSE.md | 21 + LICENSE.txt | 20 + PROPS_FOR_ESCAPE_ROOM_2_0.md | 74 ++++ Props.md | 326 ++++++++++++++ PropsAction.md | 57 +++ PropsData.md | 76 ++++ README.md | 224 ++++++++++ examples-shot.png | Bin 0 -> 92195 bytes .../BlinkOnBridgeProps/BlinkOnBridgeProps.ino | 109 +++++ .../BlinkOnBridgePubSub.ino | 220 ++++++++++ .../BlinkOnEthernetProps.ino | 113 +++++ .../BlinkOnWifiProps/BlinkOnWifiProps.ino | 105 +++++ install-shot.png | Bin 0 -> 27378 bytes keywords.txt | 59 +++ library.properties | 9 + src/ArduinoProps.cpp | 11 + src/ArduinoProps.h | 26 ++ src/BridgeProps.cpp | 28 ++ src/BridgeProps.h | 28 ++ src/EthernetProps.cpp | 22 + src/EthernetProps.h | 29 ++ src/Props.cpp | 191 ++++++++ src/Props.h | 92 ++++ src/PropsAction.cpp | 66 +++ src/PropsAction.h | 43 ++ src/PropsData.cpp | 136 ++++++ src/PropsData.h | 90 ++++ src/WifiProps.cpp | 29 ++ src/WifiProps.h | 36 ++ 36 files changed, 3188 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 ArduinoProps.sln create mode 100644 ArduinoProps.vcxitems create mode 100644 ArduinoProps.vcxitems.filters create mode 100644 EXAMPLES.md create mode 100644 LICENSE create mode 100644 LICENSE.md create mode 100644 LICENSE.txt create mode 100644 PROPS_FOR_ESCAPE_ROOM_2_0.md create mode 100644 Props.md create mode 100644 PropsAction.md create mode 100644 PropsData.md create mode 100644 README.md create mode 100644 examples-shot.png create mode 100644 examples/BlinkOnBridgeProps/BlinkOnBridgeProps.ino create mode 100644 examples/BlinkOnBridgePubSub/BlinkOnBridgePubSub.ino create mode 100644 examples/BlinkOnEthernetProps/BlinkOnEthernetProps.ino create mode 100644 examples/BlinkOnWifiProps/BlinkOnWifiProps.ino create mode 100644 install-shot.png create mode 100644 keywords.txt create mode 100644 library.properties create mode 100644 src/ArduinoProps.cpp create mode 100644 src/ArduinoProps.h create mode 100644 src/BridgeProps.cpp create mode 100644 src/BridgeProps.h create mode 100644 src/EthernetProps.cpp create mode 100644 src/EthernetProps.h create mode 100644 src/Props.cpp create mode 100644 src/Props.h create mode 100644 src/PropsAction.cpp create mode 100644 src/PropsAction.h create mode 100644 src/PropsData.cpp create mode 100644 src/PropsData.h create mode 100644 src/WifiProps.cpp create mode 100644 src/WifiProps.h diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b248083 --- /dev/null +++ b/.gitignore @@ -0,0 +1,349 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ diff --git a/ArduinoProps.sln b/ArduinoProps.sln new file mode 100644 index 0000000..3ae816d --- /dev/null +++ b/ArduinoProps.sln @@ -0,0 +1,19 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29418.71 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ArduinoProps", "ArduinoProps.vcxitems", "{45155B06-E8AA-4EA0-8802-94310DB08612}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {2F208AC8-057F-45ED-BFFE-924E36BD9E8D} + EndGlobalSection +EndGlobal diff --git a/ArduinoProps.vcxitems b/ArduinoProps.vcxitems new file mode 100644 index 0000000..f27c56a --- /dev/null +++ b/ArduinoProps.vcxitems @@ -0,0 +1,56 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + {45155b06-e8aa-4ea0-8802-94310db08612} + + + + %(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ArduinoProps.vcxitems.filters b/ArduinoProps.vcxitems.filters new file mode 100644 index 0000000..cd43086 --- /dev/null +++ b/ArduinoProps.vcxitems.filters @@ -0,0 +1,92 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;s + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx + + + {a17437ae-fae9-404e-b013-d8b8094f7732} + + + + + + + + + + + + + + + + + Examples + + + Examples + + + Examples + + + Examples + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + + + \ No newline at end of file diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..b3fe0e2 --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,409 @@ +# ArduinoProps library examples +An adaptation of the internal led Blink example (https://www.arduino.cc/en/tutorial/blink) as a simple MQTT props. + +1. **BlinkOnBridgeProps**: the Blink example on a Yun props with *ArduinoProps library* +2. **BlinkOnEthernetProps**: the Blink example on an Ethernet props with *ArduinoProps library* +3. **BlinkOnWifiProps**: the Blink example on a Wifi props with *ArduinoProps library* +4. **BlinkOnBridgePubSub**: the Blink example on props using *PubSubClient* directly + +MQTT messages are received asynchronously therefore to keep the sketch responsive to MQTT commands, calls to delay() should be avoided (except short ones, say < 100milliseconds). + +Asynchronous-like behavior is obtained using *PropsAction*, *TimedAction* or *VariableTimedAction*. + +*Copy and change any of these sketches to build your own Arduino connected props, you will only be limited by your imagination.* + + +## 1. *BlinkOnBridgeProps*: the Blink example on a Yun props with *ArduinoProps library* + +With *Props* class, the code is the most readable. All MQTT stuff are hidden in *Props* calls, your processing code is easier to write and maintain. + +```csharp +/* BlinkOnBridgeProps.ino + MIT License (c) Marie Faure + + Adapt the Blink example (https://www.arduino.cc/en/tutorial/blink) as a + simple MQTT props. Avoid delay() calls (except short ones) in loop() to + ensure CPU for MQTT protocol. Use PropsAction checks instead. + + Copy and change it to build your fist Arduino connected props, you will + only be limited by your imagination. + + Requirements: install ArduinoProps.zip library. +*/ +#include +#include "ArduinoProps.h" + +// If you're running our Escape Room control software (Room 2.0) you have to respect +// prpos inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox +// https://live-escape.net/go/room + +BridgeProps props(u8"Arduino Contrôleur", // as MQTT client id, should be unique per client for given broker + u8"Room/Demoniak/Props/Arduino Contrôleur/inbox", + u8"Room/Demoniak/Props/Arduino Contrôleur/outbox", + "192.168.1.42", // your MQTT server IP address + 1883); // your MQTT server port; + +PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true); +PropsDataLogical led(u8"led"); + +void clignote(); // forward +PropsAction clignoteAction = PropsAction(1000, clignote); + +void setup() +{ + Bridge.begin(); + //updateBrokerAdressFromFile("/root/broker", &props); // if you're running our Escape Room control software (Room 2.0) + + props.addData(&clignoter); + props.addData(&led); + + props.begin(InboxMessage::run); + + pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output + + // At this point, the broker is not connected yet +} + +void loop() +{ + props.loop(); + + led.setValue(digitalRead(LED_BUILTIN)); // read I/O + + clignoteAction.check(); // do your stuff, don't freeze the loop with delay() calls +} + +void clignote() +{ + if (clignoter.value()) { + led.setValue(!led.value()); + digitalWrite(LED_BUILTIN, led.value() ? HIGH : LOW); + } +} + +void InboxMessage::run(String a) { + + if (a == u8"app:startup") + { + props.sendAllData(); + props.sendDone(a); + } + else if (a == "clignoter:1") + { + clignoter.setValue(true); + + props.sendAllData(); // all data change, we don't have to be selctive then + props.sendDone(a); // acknowledge props command action + } + else if (a == "clignoter:0") + { + clignoter.setValue(false); + + props.sendAllData(); // all data change, we don't have to be selctive then + props.sendDone(a); // acknowledge props command action + } + else + { + // acknowledge omition of the props command + props.sendOmit(a); + } +} + +void updateBrokerAdressFromFile(const char* broker_file, BridgeProps* props) +{ + // broker IP address is stored in Linino file systems and updated with ssh command by Room 2.0 + IPAddress ip; + + Process _process; + _process.begin("cat"); + _process.addParameter(broker_file); // for ssh remotely set broker address + _process.run(); // run the process and wait for its termination + String b; + while (_process.available() > 0) { + char c = _process.read(); + b += c; + } + b.trim(); + + if (ip.fromString(b.c_str())) props->setBrokerIpAddress(ip); +} + +``` + +### Memory imprint on Arduino Yun: +```bash +The sketch uses 16720 bytes (58%) of the program storage space. The maximum is 28672 bytes. +Global variables use 1122 bytes (43%) of dynamic memory, which leaves 1438 bytes for local variables. The maximum is 2560 bytes. + +``` + +### Memory imprint on *Dragino Yun + Arduino Mega 2560: +```bash +The sketch uses 14030 bytes (5%) of the program storage space. The maximum is 258048 bytes. +Global variables use 982 bytes (11%) of dynamic memory, which leaves 7210 bytes for local variables. The maximum is 8192 bytes. + +``` + + +## 2. *BlinkOnEthernetProps*: the Blink example on an Ethernet props with *ArduinoProps library* + +Sketch with *EthernetProps* differs slightly from code with *BridgeProps*. + +The library comes with a number of example sketches. See **File > Examples > ArduinoProps** within the Arduino IDE application after installing the *ArduinoProps* library from the `.zip` file. + +![](examples-shot.png) + +## 3. *BlinkOnWifiProps*: the Blink example on a Wifi props with *ArduinoProps library* + +Sketch with *WifiProps* differs slightly from code with *BridgeProps*. + + +## 4. *BlinkOnBridgePubSub*: the Blink example on props using *PubSubClient* directly + +Using *PubSubClient* directly does not save much memory and makes the sketch code less readable, the processing code will be a bit lost in the MQTT code + +However, this can help in special cases. + +```csharp +/* + Name: BlinkOnBridgePubSub.ino + Created: 29/10/2019 16:51:55 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Adapt the Blink example (https://www.arduino.cc/en/tutorial/blink) as a + simple MQTT props with PubSubClient. +*/ +#include +#include +#include +#include +#include + +#define BROKER "192.168.1.42" // your MQTT server IP address +#define PROPS_NAME u8"Arduino Contrôleur" // as MQTT client id, should be unique per client for given broker + +// If you're running our Escape Room control software (Room 2.0) you have to respect +// props inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox +// https://github.com/fauresystems/escape-room#room-control-software +#define PROPS_INBOX u8"Room/Demoniak/Props/Arduino Contrôleur/inbox" +#define PROPS_OUTBOX u8"Room/Demoniak/Props/Arduino Contrôleur/outbox" + +// Yun can store broker IP address in Linino file systems, and updatred with ssh command +#define YUN_BROKER_FILE "/root/broker" + +void publishAll(); // forward +void publishChanges(); // forward + +class Blinking : public VariableTimedAction { +private: + //this method will be called at your specified interval + unsigned long run() { + //swicth LED if blinking mode + if (blink) { + digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); + } + + //return code of 0 indicates no change to the interval + //if the interval must be changed, then return the new interval + return 0; + } + +public: + bool blink; +}; + +class PublishAllData : public VariableTimedAction { +private: + unsigned long run() { + publishAll(); + return 0; + } +}; + +class PublishChangedData : public VariableTimedAction { +private: + unsigned long run() { + publishChanges(); + return 0; + } +}; + +Blinking blinking; + +bool led_ref; +bool blink_ref; + +BridgeClient _ethClient; +PubSubClient _client(_ethClient); +IPAddress _brokerAddress; + +void callback(char*, byte*, unsigned int); // forward + +// MQTT +unsigned long lastReconnection(0L); +PublishAllData publishAllData; +PublishChangedData publishChangedData; + +void setup() +{ + Bridge.begin(); + + Process p; + p.begin("cat"); + p.addParameter(YUN_BROKER_FILE); + p.run(); // run the process and wait for its termination + String b; + while (p.available() > 0) { + char c = p.read(); + b += c; + } + b.trim(); + + if (!_brokerAddress.fromString(b.c_str())) _brokerAddress.fromString(BROKER); + _client.setServer(_brokerAddress, 1883); + _client.setCallback(callback); + + publishAllData.start(30000); + publishChangedData.start(400); + + blinking.blink = true; + blinking.start(1000); + blink_ref = false; + + pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output + digitalWrite(LED_BUILTIN, HIGH); + led_ref = LOW; + + // At this point, the broker is not connected yet +} + +void loop() +{ + if (_client.connected()) + { + _client.loop(); + } + else if (millis() > lastReconnection) + { + lastReconnection += 5000L; + + if (_client.connect(PROPS_NAME, PROPS_OUTBOX, 2, true, "DISCONNECTED")) + { + _client.publish(PROPS_OUTBOX, "CONNECTED", true); + _client.subscribe(PROPS_INBOX, 1); // max QoS is 1 for PubSubClient subsciption + lastReconnection = 0L; + } + } + + // automation code should be here (nothing to do for this example) + + VariableTimedAction::updateActions(); +} + +void publishAll() +{ + String buf = "DATA"; + + bool led = digitalRead(LED_BUILTIN); + + buf += u8" led=" + (led ? String("1") : String("0")); + led_ref = led; + + buf += u8" clignote=" + (blinking.blink ? String("oui") : String("non")); + blink_ref = blinking.blink; + + _client.publish(PROPS_OUTBOX, buf.c_str()); +} + +void publishChanges() +{ + String buf = "DATA"; + + bool led = digitalRead(LED_BUILTIN); + + if (led != led_ref) + { + buf += u8" led=" + (led ? String("1") : String("0")); + led_ref = led; + } + + if (blinking.blink != blink_ref) + { + buf += u8" clignote=" + (blinking.blink ? String("oui") : String("non")); + blink_ref = blinking.blink; + } + + if (buf.length() > 4) + _client.publish(PROPS_OUTBOX, buf.c_str()); +} + +void publishDone(String a) +{ + a = "DONE " + a; + _client.publish(PROPS_OUTBOX, a.c_str()); +} + +void onInboxMessage(String a) { + + if (a == u8"app:startup") + { + publishAll(); + publishDone(a); + } + else if (a == "clignoter:1") + { + digitalWrite(LED_BUILTIN, HIGH); + blinking.blink = true; + + publishAll(); // all data change, we don't have to be selctive then + publishDone(a); // acknowledge props command action + } + else if (a == "clignoter:0") + { + digitalWrite(LED_BUILTIN, LOW); + blinking.blink = false; + + publishAll(); // all data change, we don't have to be selctive then + publishDone(a); // acknowledge props command action + } + else + { + // acknowledge omition of the props command + _client.publish(PROPS_OUTBOX, String("OMIT " + a).c_str()); + } +} + +void callback(char* topic, byte* payload, unsigned int len) +{ + if (len) + { + char* p = (char*)malloc(len + 1); + memcpy(p, payload, len); + p[len] = '\0'; + if (String(p) == "@PING") + _client.publish(PROPS_OUTBOX, "PONG"); + else + onInboxMessage(p); + free(p); + } +} + +``` + +### Memory imprint on Arduino Yun: +```bash +The sketch uses 15930 bytes (55%) of the program storage space. The maximum is 28672 bytes. +Global variables use 1013 bytes (39%) of dynamic memory, which leaves 1547 bytes for local variables. The maximum is 2560 bytes. + +``` + + +## Author + +**Marie FAURE** (Oct 18th, 2019) +* company: FAURE SYSTEMS SAS +* mail: *dev at faure dot systems* +* github: fauresystems +* web: Live Escape Grenoble \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b1d1f0d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 fauresystems + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..9b74d6b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +## MIT License + + *Copyright (c) 2019 Marie Faure \* + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..51af46c --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2019 Marie Faure + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/PROPS_FOR_ESCAPE_ROOM_2_0.md b/PROPS_FOR_ESCAPE_ROOM_2_0.md new file mode 100644 index 0000000..84d4bc5 --- /dev/null +++ b/PROPS_FOR_ESCAPE_ROOM_2_0.md @@ -0,0 +1,74 @@ +# Arduino Props for Escape Room 2.0 + +The *ArduinoProps library* for Arduino has been designed to build *escape room 2.0 props*. + +Specificities of props for escape rooms: +- very high uptime (near 100%) +- fast problem fixing +- real-time (human scale) notification and control +- fallback controls +- low Total Cost of Ownership + +Benefits and specificities of the *ArduinoProps library* +- rely on standard MQTT messaging protocol for IoT +- simplify and ease Arduino coding +- constant sustaining of Arduino code (YUN) +- real-time notification, monitoring and control (human scale) + +# Escape room 2.0 constraints on props +Escape game are opened 7 days a week and particularly booked during weekends and usually until midnight. Therefore their uptime must be close to 100% and incidents must be worked-around immediately by the game master (manual commands). + +Escape Room Props operation and performance impact directly escape game customer experience. Therefore notifications must be processed in human-scale real-time, automatically or manually by a non-technological user (the game master). + +Escape Room Props TCO (Total Cost of Ownership) must be affordable by escape games (SOHO costs for small businesses), processing is usually supported by Arduino or Raspberry boards. IT professional 24/7 support must be avoided for cost reasons. Therefore props code must be the most reliable. + + +# *ArduinoProps library* for escape room 2.0 +[MQTT](http://mqtt.org/) is a machine-to-machine (M2M)/"Internet of Things" connectivity protocol. It was designed as an extremely lightweight publish/subscribe messaging transport. + +*ArduinoProps library* relies on *[Nick O'Leary PubSubClient](https://github.com/knolleary/pubsubclient)*, a simple publish/subscribe messaging Arduino library. [PubSubClient](https://github.com/knolleary/pubsubclient) is a MQTT client with a few limitations: + +- It can only publish QoS 0 messages. It can subscribe at QoS 0 or QoS 1. +- The maximum message size, including header, is 128 bytes by default. This is configurable via MQTT_MAX_PACKET_SIZE in PubSubClient.h. +- The keepalive interval is set to 15 seconds by default. This is configurable via MQTT_KEEPALIVE in PubSubClient.h. +- The client uses MQTT 3.1.1 by default. It can be changed to use MQTT 3.1 by changing value of MQTT_VERSION in PubSubClient.h. + +How does *ArduinoProps library* use MQTT efficiently in escape rooms: + +- Every props has two MQTT topics assigned as inbox and outbox. +- More MQTT topics can be used to share game countdown, scenario or any common useful dynamic data for the escape game. +- Notifying the props is done by publishing a message on its inbox topic. +- Other props of software monitor can subscribe to the props outbox topic to receive all its notifications. +- *ArduinoProps library* includes a ping process at application level, in LAN network the round-trip-delay is around 50 milliseconds. +- *ArduinoProps library* is defaulted to publish QoS 0 messages and to subscribe at QoS 1. + +*ArduinoProps library* wraps [PubSubClient](https://github.com/knolleary/pubsubclient) to make Arduino code the most simple and readable for better sustainability. + +# Arduino YUN and YUN shields +Arduino YUN is a Arduino with Leonardo MCU/GPIO and Linux SoC. + +Arduino YUN shield and Dragino YUN shield are YUN Linux SoC to be plugged onto Arduino Uno, Mega or Due. + +**Arduino YUN advantage is that you can upload AVR program from the Arduino IDE at any moment during the Props lifetime**: +- change sensor settings when mechanics is undermined +- tune whole challenge processing + +Arduino YUN can be rebooted or its MCU resetted remotely with simple SSH commands. + +Moreover, SSH command can be used to remotely change the MQTT server IP address, *ArduinoProps library* read this address at sketch start. + +Therefore, Arduino YUN or YUN shield is the preferred solution for props with Arduino boards because it will be very easy to sustain over time and greatly facilitates fallback plans to ensure maximum uptime. + +# Arduino Ethernet and Wifi +Arduino with Ethernet or Wifi shield is suitable to build an Escape Room Props, however the sketch must be uploaded with an USB cable, so it is not convenient for props design and worse for props in production. + +Therefore I strongly advise Arduino YUN or YUN shield for Escape Room props. + + +## Author + +**Marie FAURE** (Oct 18th, 2019) +* company: FAURE SYSTEMS SAS +* mail: *dev at faure dot systems* +* github: fauresystems +* web: Live Escape Grenoble \ No newline at end of file diff --git a/Props.md b/Props.md new file mode 100644 index 0000000..57e8fc0 --- /dev/null +++ b/Props.md @@ -0,0 +1,326 @@ +# *Props* class reference + +*Props* is a base class for 3 connected props, depending on their network interface: +* ***BridgeProps*** for Yun board and Yun shield +* ***EthernetProps*** for Ethernet shield +* ***WifiProps*** for Wifi shield + +*Props* class reference: +1. Define the *Props* + * Defining a *BridgeProps* + * Defining an *EthernetProps* + * Defining a *WifiProps* +2. Setup the *Props* + * Change timings +3. Call in sketch *loop()* +4. Implement `InboxMessage::run()` callback +5. Update broker address from SSH (*BridgeProps*) +6. Examples + + +## 1. Defines the *Props* +Defining a Bridge (Yun), Ethernet or Wifi *Props* is slighlty different. + +### Defining a *BridgeProps* +```csharp +#include +#include "ArduinoProps.h" + +// If you're running our Escape Room control software (Room 2.0) you have to respect +// prpos inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox +// https://live-escape.net/go/room + +BridgeProps props(u8"Arduino Contrôleur", // as MQTT client id, should be unique per client for given broker + u8"Room/Demoniak/Props/Arduino Contrôleur/inbox", + u8"Room/Demoniak/Props/Arduino Contrôleur/outbox", + "192.168.1.42", // your MQTT server IP address + 1883); // your MQTT server port; + +PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true); +PropsDataLogical led(u8"led"); + +void clignote(); // forward +PropsAction clignoteAction = PropsAction(1000, clignote); +``` +Create *BridgeProps* instance with MQTT parameters, then create *PropsData* instances and *PropsActions*. + +The *Bridge* instance is created by `#include `. + +### Defining an *EthernetProps* +```csharp +#include +#include +#include "ArduinoProps.h" + +byte mac[] = { 0xFC, 0xFA, 0xEA, 0x00, 0x00, 0x02 }; //<<< MAKE SURE IT'S UNIQUE IN YOUR NETWORK!!! +String ip = "192.168.1.19"; //<<< ENTER YOUR IP ADDRESS HERE ("" for DHCP) + +// Builtin led is not available with the shield +#undef LED_BUILTIN +#define LED_BUILTIN 8 + +// If you're running our Escape Room control software (Room 2.0) you have to respect +// prpos inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox +// https://live-escape.net/go/room + +EthernetProps props(u8"Arduino Contrôleur", // as MQTT client id, should be unique per client for given broker + u8"Room/Demoniak/Props/Arduino Contrôleur/inbox", + u8"Room/Demoniak/Props/Arduino Contrôleur/outbox", + "192.168.1.42", // your MQTT server IP address + 1883); // your MQTT server port; + +PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true); +PropsDataLogical led(u8"led"); + +void clignote(); // forward +PropsAction clignoteAction = PropsAction(1000, clignote); +``` +Create MAC address (which must be unique, it's the hardware ID in the netwprk). + +Create *EthernetProps* instance with MQTT parameters, then create *PropsData* instances and *PropsActions*. + +The *Ethernet* instance is created by `#include `. + +### Defining a *WifiProps* +```csharp +#include "ArduinoProps.h" + +// Setup your WiFi network +const char* ssid = ""; +const char *passphrase = ""; + +// Builtin led is not available with the shield +#undef LED_BUILTIN +#define LED_BUILTIN 8 + +// If you're running our Escape Room control software (Room 2.0) you have to respect +// prpos inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox +// https://live-escape.net/go/room + +WifiProps props(u8"Arduino Contrôleur", // as MQTT client id, should be unique per client for given broker + u8"Room/Demoniak/Props/Arduino Contrôleur/inbox", + u8"Room/Demoniak/Props/Arduino Contrôleur/outbox", + "192.168.1.42", // your MQTT server IP address + 1883); // your MQTT server port; + +PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true); +PropsDataLogical led(u8"led"); + +void clignote(); // forward +PropsAction clignoteAction = PropsAction(1000, clignote); +``` +The *AduinoProps* library uses **WiFiNINA** WiFi library for **Arduino Uno WiFi Rev 2**, **Arduino NANO 33 IoT**, **Arduino MKR 1010** and **Arduino MKR VIDOR 4000**. + +Create *WifiProps* instance with MQTT parameters, then create *PropsData* instances and *PropsActions*. + +#### MQTT topics +The props listens to the *inbox* topic for command messages and send data and messages into the *outbox* topic. + +If you're running Live Escape Room 2.0 control software, you have to respect *inbox*/*outbox* syntax: + +Room 2.0 *inbox* topic: +```csharp +format: Room/[escape room name]/Props/[props name]/inbox +example: u8"Room/Demoniak/Props/Arduino Contrôleur/inbox" +``` +Room 2.0 *outbox* topic: +```csharp +format: Room/[escape room name]/Props/[props name]/outbox +example: u8"Room/Demoniak/Props/Arduino Contrôleur/outbox" +``` +See [README.md](README.md): **4. Application protocol for escape room 2.0 props**. + +#### *PropsData* instances +*PropsData* provides a facility to monitor data sent into MQTT outbox. + +See [PropsData.md](PropsData.md) for *PropsData* reference. + +#### *PropsAction* instances +To maintain a non-blocking asynchronous behavior of your props, use *PropsAction* objects to trigger action periodically. + +Reading I/O and simple computing (arithmetics) can be done at every sketch loop but long computing have to run periodically. + +For example, to blink a led, don't block the skecth with a `delay(1000)` call to switch the output: instead, ***check if 1 second is elapsed to switch the output***. + +See [PropsAction.md](PropsData.md) for *PropsAction* reference. + + +## 2. Setup the *Props* +`InboxMessage::run` is the callback executed when a message is received in the MQTT inbox. +```csharp +void setup() +{ + // CODE TO BEGIN THE INTERNET CONNECTION + ... see below the different code for BridgeProps, EthenetProps and WifiProps + + // SAME CODE FOR ALL PROPS + props.addData(&clignoter); + props.addData(&led); + + props.begin(InboxMessage::run); + + pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output + + // At this point, the broker is not connected yet +} + +``` + +*BridgeProps*, *EthenetProps* and *WifiProps* has the exactly same setup: + +- add *PropsData* references to the props +- call `props.begin(InboxMessage::run)` + +The setup code is slighlty different to start the network client (bridge, ethernet of wifi): +```csharp +void setup() +{ +#if defined(BRIDGEPROPS_H) + Bridge.begin(); + updateBrokerAdressFromFile("/root/broker", &props); // if you're running our Escape Room control software (Room 2.0) + +#elif defined(ETHERNETPROPS_H) + // can do more static IP configuration + //Ethernet.setSubnetMask(IPAddress()); + //Ethernet.setGatewayIP(IPAddress()); + //Ethernet.setDnsServerIP(IPAddress()); + + IPAddress ipa; + if (!ipa.fromString(ip)) { + // attempt a DHCP connection: + if (!Ethernet.begin(mac)) { + // if DHCP fails, start with a hard-coded address: + Ethernet.begin(mac, IPAddress(10, 90, 90, 239)); + } + } + else { + Ethernet.begin(mac, ipa); + } + delay(1500); // time for shield stuff + +#elif defined(WIFIPROPS_H) + // can do more static IP configuration + //WiFi.config(IPAddress(), IPAddress(), IPAddress()); + //WiFi.config(IPAddress(), IPAddress()); + //WiFi.config(IPAddress()); + + WiFi.begin(ssid, passphrase); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + } +#endif + ... +} + +``` + +### Change timings +Default timings are: +* check data changes every 400 milliseconds +* *Props* remains silent for up to 30 seconds + +Timings can be changed anywhere in the code with `resetIntervals()`: +```csharp + props.resetIntervals(250, 15); // check data changes every 250 milliseconds, silent up to 15 seconds +``` + + +## 3. Call in sketch *loop()* +```csharp +void loop() +{ + props.loop(); + + led.setValue(digitalRead(LED_BUILTIN)); // read I/O + + clignoteAction.check(); // do your stuff, don't freeze the loop with delay() calls +} + +``` +Couldn't be more simple! + +Just call `props.loop()` when entering the sketch `loop()`, it will maintain MQTT broker connection and run callback when receiving MQTT messages. + + +## 4. Implement `InboxMessage::run()` callback +`InboxMessage::run` is the callback executed when a message is received in the MQTT inbox, you have to implement it. + +Example for Room 2.0 commands: +```csharp +void InboxMessage::run(String a) { + + if (a == u8"app:startup") + { + props.sendAllData(); + props.sendDone(a); + } + else if (a == "clignoter:1") + { + clignoter.setValue(true); + + props.sendAllData(); // all data change, we don't have to be selctive then + props.sendDone(a); // acknowledge props command action + } + else if (a == "clignoter:0") + { + clignoter.setValue(false); + + props.sendAllData(); // all data change, we don't have to be selctive then + props.sendDone(a); // acknowledge props command action + } + else + { + // acknowledge omition of the props command + props.sendOmit(a); + } +} +``` + + +## 5. Update broker address from SSH (*BridgeProps*) +Room 2.0 use a SSH command to change remotely the broker IP address (in /root/broker file) and to relaunch to sketch: +```bash +ssh 'echo 192.168.1.14> /root/broker && reset-mcu' +``` +On the *BridgeProps* you can read the broker IP address from Linino filesystem with this code: +```csharp +void updateBrokerAdressFromFile(const char* broker_file, BridgeProps* props) +{ + // broker IP address is stored in Linino file systems and updated with ssh command by Room 2.0 + IPAddress ip; + + Process _process; + _process.begin("cat"); + _process.addParameter(broker_file); // for ssh remotely set broker address + _process.run(); // run the process and wait for its termination + String b; + while (_process.available() > 0) { + char c = _process.read(); + b += c; + } + b.trim(); + + if (ip.fromString(b.c_str())) props->setBrokerIpAddress(ip); +} +``` + + +## 6. Examples +The library comes with a number of example sketches. See **File > Examples > ArduinoProps** within the Arduino IDE application. + +See [EXAMPLES.md](EXAMPLES.md) for an adaptation of the Blink example (https://www.arduino.cc/en/tutorial/blink) as a simple MQTT props: + +1. **BlinkOnBridgeProps**: the Blink example on a Yun props with *ArduinoProps library* +2. **BlinkOnEthernetProps**: the Blink example on an Ethernet props with *ArduinoProps library* +3. **BlinkOnWifiProps**: the Blink example on a Wifi props with *ArduinoProps library* +4. **BlinkOnBridgePubSub**: the Blink example on props using *PubSubClient* directly + + +## Author + +**Marie FAURE** (Oct 18th, 2019) +* company: FAURE SYSTEMS SAS +* mail: *dev at faure dot systems* +* github: fauresystems +* web: Live Escape Grenoble \ No newline at end of file diff --git a/PropsAction.md b/PropsAction.md new file mode 100644 index 0000000..73b636e --- /dev/null +++ b/PropsAction.md @@ -0,0 +1,57 @@ +# *PropsAction* class reference + +*PropsAction* brings asynchronous-like behavior to sketch `loop()`. + + +## Constructors +* `PropsAction()` +* `PropsAction(unsigned long interval, void (*function)())` + +`function` is the callback executed at every `interval` milliseconds. + +## Members +* `void reset(unsigned long interval)` +* `void disable()` +* `void enable()` +* `void check()` +* `bool tick()` +* `unsigned long getInterval()` + +## Usage + +### Create a callback +```csharp +void clignote() +{ + if (clignoter.value()) { + digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); + led.setValue(digitalRead(LED_BUILTIN)); + } +} +``` + +### Create a *PropsAction* instance +```csharp +PropsAction clignoteAction = PropsAction(1000, clignote); // interval every 1000 milliseconds +``` + +### Call the instance `check()` method ine the sketch `loop()` +```csharp +void loop() +{ + ... + + clignoteAction.check(); // do your stuff, don't freeze the loop with delay() calls + + ... +} +``` + + +## Author + +**Marie FAURE** (Oct 18th, 2019) +* company: FAURE SYSTEMS SAS +* mail: *dev at faure dot systems* +* github: fauresystems +* web: Live Escape Grenoble \ No newline at end of file diff --git a/PropsData.md b/PropsData.md new file mode 100644 index 0000000..0eef3cd --- /dev/null +++ b/PropsData.md @@ -0,0 +1,76 @@ +# *PropsData* reference + +*PropsData* tracks data changes and provides escape room 2.0 propocol `DATA` ready string format. + +See **4. Application protocol for escape room 2.0 props** in [README.md](README.md). + +*PropsData* is an interface (base class) implemented by 4 classes: +* ***PropsDataDecimal*** +* ***PropsDataInteger*** +* ***PropsDataLogical*** +* ***PropsDataText*** + +## Constructors +* `PropsDataDecimal(const char *, uint8_t digits = 2, double precision = 0.1, double initial = 0)` +* `PropsDataInteger(const char *, double precision = 0.1, long initial = 0)` +* `PropsDataLogical(const char *, const char *trueval = NULL, const char *falseval = NULL, bool initial = false)` +* `PropsDataText(const char *)` + +By default for *PropsDataLogical*, `trueval` is `"1"` band `falseval` is `"0"`. + +For *PropsDataDecimal*, `digits` is the number of decimal digits in the display string. + +For *PropsDataDecimal* and *PropsDataInteger*, `precision` is the percentage threshold for deciding whether a change has occurred or not. + +## Interface +* `String fetch()` +* `String fetchChange()` + +*fetch()* always returns the `"var=value "` string ready to be sent in a `DATA` protocol message and update the `_previous` value. + +*fetchChange()* checks whether a change has occurred or not. If a change occured, returns the `"var=value "` string ready to be sent in a `DATA` protocol message and update the `_previous` value. If no change occured, returns and empty string. + +## Accessors and Mutators +* `void setValue(const double|long|bool|String)` +* `double|long|bool|String value() const` + +Accessors and Mutators are typed according to *PropsData* type. + +## Usage + +### Create a *PropsData* instance +```csharp +PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true); +``` + +### Add the *PropsData* instance to managed data of the *Props* +```csharp +void setup() +{ + ... + + Props::addManagedLogicalData(&clignoter); + + ... +} +``` + +### Use the *PropsData* instance anywhere +```csharp +clignoter.setValue(true); + +... + +if (clignoter.value()) { + digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); + led.setValue(digitalRead(LED_BUILTIN)); +} +``` + +## Author + +**Marie FAURE** (Oct 18th, 2019) +* company: FAURE SYSTEMS SAS +* mail: *dev at faure dot systems* +* github: fauresystems +* web: Live Escape Grenoble \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e12501 --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +# ArduinoProps library +1. Installation and usage +2. Escape room 2.0 props with Arduino +3. *ArduinoProps library* value for escape room 2.0 +4. Application protocol for escape room 2.0 props +5. Compatible hardware +6. Advantages of Arduino Yun and Dragino Yun shield +7. Advantages of the *ArduinoProps library* +8. Limitations due to *PubSubClient* +9. Examples + + +## 1. Installation and usage +Download `ArduinoProps-master.zip` from GitHub and use **Sketch > Include Library > Add .ZIP Library...** + +![](install-shot.png) + +Add `#include "ArduinoProps.h"` on top of your sketch and start coding. Reading [EXAMPLES.md](EXAMPLES.md) will be helpful. + +|ArduinoProps library documentation | +|-----------------------------------------------| +| [This README file](README.md) | +| [Props class reference](Props.md) | +| [PropsLite class reference](PropsLite.md) | +| [PropsData class reference](PropsData.md) | +| [PropsAction class reference](PropsAction.md) | +| [ArduinoProps library examples](EXAMPLES.md) | + + +## 2. Escape room 2.0 props with Arduino + +The *ArduinoProps library* allows to build *escape room 2.0 props* easily with Arduino and particularly with Arduino Yun or Arduino Mega 2560 + Dragino Yun shield however it works with Ethernet and Wifi shields too. + +The *ArduinoProps library* relies on *Nick O'Leary PubSubClient* library for MQTT. MQTT is a lightweight publish/subscribe messaging protocol designed for M2M (machine to machine) telemetry in low bandwidth environments. + +You will find Raspberry Pi libraries for escape room 2.0 props at https://github.com/fauresystems + + +## 3. *ArduinoProps library* value for escape room 2.0 +*Nick O'Leary PubSubClient* library is sufficient to write escape room 2.0 props Arduino code. + +The *ArduinoProps library* relies on *Nick O'Leary PubSubClient* for MQTT messaging and provides a framework and protocol for helping and accelerating the coding of Arduino props. If you will help build simple and complex props with reliable code. + +Since 2016, *ArduinoProps library* is used extensively in many escape rooms with up to 20 props per room (which is no limit at all), with a high level of availability and sustainability. + +### Inbox/oubox messaging +Instead of usual MQTT topic structure which sends every data value in a different topic for every variable, *ArduinoProps library* sends data via a unique outbox topic. + +Props receives commands in its inbox MQTT topic. + +Props can subscribe to any other MQTT topic to receive othe information, for example the escape game scenario (English/French/Kids) or the game clock countdown. + +### Escape room structured MQTT topics +If you're running our Escape Room control software *Room 2.0* to supervise the escape room in real-time you have to respect its syntax for inbox/outbnox MQTT topics: +```csharp +Room/[escape room name]/Props/[propsname]/inbox|outbox + +example: + Room/Demoniak/Props/Arduino Contrôleur/inbox + Room/Demoniak/Props/Arduino Contrôleur/outbox +``` +MQTT supports UTF-8 encoding. + +### Sending messages only when appropriate +Usually Arduino IoT sketches send MQTT data for every variable at every loop. When the loop is fast, you can get tons of useless messages, and usually a sleeping delay is added at every loop to slow down the message flow ; this can slow down significantly the props response time to human supervision and automation commands. + +While Arduino sketch is sending a message it isn't doing props sensor/actuator processing, and it takes about 20 to 40 milliseconds to send a message. With too many messsages it's too much processing time wasted. + +*ArduinoProps library* sends all data periodically every 30 seconds (default parameter) and sends data changes over a period of time you choose (typical 400 milliseconds for real-time behavior, but you may choose every 100 milliseconds, 1 or 3 seconds: your choice for your application). + +Therefore the minimum processing time is used to send messages. + +Tracking data changes could make Arduino code difficult to read and maintain but the. *ArduinoProps library* will hide this mechanism and make the code much more readable. + +Some analog data can change at every loop but not significantly, for example U=2.77 volts while showing this when it changes more than 10% could be sufficient. *ArduinoProps library* offers a nice syntax to take care of this significance, with simple code. + +### Maintaining MQTT server connection +MQTT connection state must be checked at erevy sketch loop as well as incoming MQTT messages. *ArduinoProps library* does all in one code line. + +And on Yun this same code line can switch MQTT server IP address (can be helpful in a fallback plan). + +### Simple human-readable text protocol +An escape room 2.0 is typically centrally controlled from a Windows PC which provides the Game Master with monitoring and control information as well as some automation. + +To unifiy escape room 2.0 development, *ArduinoProps library* provides a simple protocol between props and room controller at application level: +```csharp + DATA -> send variables to control + MESG -> send text to display in control + DONE -> aknowledge a command has been performed + OMIT -> aknowledge a command has been ignored + OVER -> notify a challenge is over + REQU -> request a command to another props + PROG -> request a control program +``` + +### Handling long message seamlessly and efficiently +MQTT standard limitations are far above what we need: +- maximum topic length 65,536 bytes (64k) +- maximum message size 268,435,456 bytes (256M) + +However, on Arduino the limit is for the addition of topic length + message length: +- Wifi shield limit is 80 bytes +- Yun limit (due to Bridge) is 120 bytes + +DATA messages when many I/O like on Arduino Mega 2560 and MESG information messages can go very long, and crash the sketch without notice at run-time. + +*ArduinoProps library* seamlessly splits long messages smartly when required. + +## 4. Application protocol for escape room 2.0 props + +A *level 7* protocol (*application level*) is necessary for connected props to report data and to be controlled for the escape room automation and game play. + +We have defined a simple human-readable text protocol. Messages are encoded in UTF-8, in Arduino sketches use: +```csharp + str = u8"la chaîne avec des caractères non Latin1"; +``` + +Messages are sent/received thru outbox/inbox MQTT topics: +```csharp + Room/[escape room name]/Props/[propsname]/inbox + Room/[escape room name]/Props/[propsname]/outbox +``` + +Props connection state is handled in the outbox topic: +* with a `DISCONNECTED` **Will** when MQTT server connection is broken +* with a `CONNECTED` **retained message** when MQTT server connection is established. + +More MQTT topics can be use for anything (room scenario, etc.). + +#### Messages sent from the Arduino props are formatted +```csharp + DATA sensor=24.26 light=on challenge=pinball + MESG Warning: sensor not detected + DONE power:1 + OMIT power:1 + OVER pinball + REQU Arduino Relay -> door:open + PROG audio-final:start +``` + +#### Message received by the Arduino props have no particular format +```csharp + power:1 for example to power on the room + porte-salon:ouvrir for example to open a door + lumière-salon:éteindre for example to switch-off a light +``` + + +#### @PING-PONG special messages +```csharp + @PING is received in inbox then the props sends just PONG in outbox + -> so the escape room controller can monitor response time at application level +``` + + +## 5. Compatible hardware +You can use the *ArduinoProps library* with any *Nick O'Leary PubSubClient* supported hardware. It uses the Arduino Ethernet Client api for interacting with the underlying network hardware. This means it Just Works with a growing number of boards and shields, including: + + - Arduino Ethernet + - Arduino Ethernet Shield + - Arduino Yun – use the included `YunClient` in place of `EthernetClient`, and + be sure to do a `Bridge.begin()` first + - Arduino WiFi Shield - if you want to send packets > 90 bytes with this shield, + enable the `MQTT_MAX_TRANSFER_SIZE` define in `PubSubClient.h`. + - Sparkfun WiFly Shield – [library](https://github.com/dpslwk/WiFly) + - TI CC3000 WiFi - [library](https://github.com/sparkfun/SFE_CC3000_Library) + - Intel Galileo/Edison + - ESP8266 + - ESP32 + +Please check at https://github.com/knolleary/pubsubclient + + +## 6. Advantages of Arduino Yun and Dragino Yun shield +In escape room 2.0, props are usually powered by Arduino Yun or Arduino Mega 2560 + Dragino Yun shield (or Raspberry Pi) because they are cheap and appropriate for DIY projects. + +***The advantage of Yun versus Ethernet/Wifi shields is that you can upload sketches remotely to the micro-controller during the whole props lifetime*** (no need to open boxes and break walls to plug an USB cable when a sketch modification is required). + +Arduino Mega 2560 + Dragino Yun shield offers much more memory and many more pins thann an Arduino Yun for same price. + + +## 7. Advantages of the *ArduinoProps library* +You can build escape room 2.0 Arduino-based props just with Nick O'Leary PubSubClient library for MQTT. + +With *ArduinoProps library*: +- create connected props quickly thanks to the application protocol and examples +- targetted for escape room props, *ArduinoProps library* can be used for any IoT project +- re-use your code when creating new escape rooms**** +- rely on *ArduinoProps library* robustness: for example, ***the library handles the Arduino Yun Bridge transfer size limitation by smartly splitting large messages (only) when necessary***. + +**Note:** sending too large messages to Arduino Bridge breaks the Arduino sketch. *ArduinoProps library* prevents this crash case. + +*ArduinoProps library* itself relies on *Nick O'Leary PubSubClient* library for MQTT. + + +## 8. Limitations due to *PubSubClient* +These limitations are not an issue for escape room props: + - It can only publish QoS 0 messages. + - It can subscribe at QoS 0 or QoS 1. + - The keepalive interval is set to 15 seconds by default. This is configurable + via `MQTT_KEEPALIVE` in `PubSubClient.h`. + - The client uses MQTT 3.1.1 by default. It can be changed to use MQTT 3.1 by + changing value of `MQTT_VERSION` in `PubSubClient.h`. + + +## 9. Examples + +The library comes with a number of example sketches. See **File > Examples > ArduinoProps** within the Arduino IDE application. + +See [EXAMPLES.md](EXAMPLES.md) for an adaptation of the Blink example (https://www.arduino.cc/en/tutorial/blink) as a simple MQTT props: + +1. **BlinkOnBridgeProps**: the Blink example on a Yun props with *ArduinoProps library* +2. **BlinkOnEthernetProps**: the Blink example on an Ethernet props with *ArduinoProps library* +3. **BlinkOnWifiProps**: the Blink example on a Wifi props with *ArduinoProps library* +4. **BlinkOnBridgePubSub**: the Blink example on props using *PubSubClient* directly + + +## Author + +**Marie FAURE** (Oct 18th, 2019) +* company: FAURE SYSTEMS SAS +* mail: *dev at faure dot systems* +* github: fauresystems +* web: Live Escape Grenoble \ No newline at end of file diff --git a/examples-shot.png b/examples-shot.png new file mode 100644 index 0000000000000000000000000000000000000000..32b48b3e2a79fa50ed142fb6a09d745501c8916e GIT binary patch literal 92195 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 literal 0 HcmV?d00001 diff --git a/examples/BlinkOnBridgeProps/BlinkOnBridgeProps.ino b/examples/BlinkOnBridgeProps/BlinkOnBridgeProps.ino new file mode 100644 index 0000000..d9fe63b --- /dev/null +++ b/examples/BlinkOnBridgeProps/BlinkOnBridgeProps.ino @@ -0,0 +1,109 @@ +/* BlinkOnBridgeProps.ino + MIT License (c) Marie Faure + + Adapt the Blink example (https://www.arduino.cc/en/tutorial/blink) as a + simple MQTT props. Avoid delay() calls (except short ones) in loop() to + ensure CPU for MQTT protocol. Use PropsAction checks instead. + + Copy and change it to build your fist Arduino connected props, you will + only be limited by your imagination. + + Requirements: install ArduinoProps.zip library. +*/ +#include +#include "ArduinoProps.h" + +// If you're running our Escape Room control software (Room 2.0) you have to respect +// prpos inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox +// https://live-escape.net/go/room + +BridgeProps props(u8"Arduino Contrôleur", // as MQTT client id, should be unique per client for given broker + u8"Room/Demoniak/Props/Arduino Contrôleur/inbox", + u8"Room/Demoniak/Props/Arduino Contrôleur/outbox", + "192.168.1.42", // your MQTT server IP address + 1883); // your MQTT server port; + +PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true); +PropsDataLogical led(u8"led"); + +void clignote(); // forward +PropsAction clignoteAction = PropsAction(1000, clignote); + +void setup() +{ + Bridge.begin(); + //updateBrokerAdressFromFile("/root/broker", &props); // if you're running our Escape Room control software (Room 2.0) + + props.addData(&clignoter); + props.addData(&led); + + props.begin(InboxMessage::run); + + pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output + + // At this point, the broker is not connected yet +} + +void loop() +{ + props.loop(); + + led.setValue(digitalRead(LED_BUILTIN)); // read I/O + + clignoteAction.check(); // do your stuff, don't freeze the loop with delay() calls +} + +void clignote() +{ + if (clignoter.value()) { + led.setValue(!led.value()); + digitalWrite(LED_BUILTIN, led.value() ? HIGH : LOW); + } +} + +void InboxMessage::run(String a) { + + if (a == u8"app:startup") + { + props.sendAllData(); + props.sendDone(a); + } + else if (a == "clignoter:1") + { + clignoter.setValue(true); + + props.sendAllData(); // all data change, we don't have to be selctive then + props.sendDone(a); // acknowledge props command action + } + else if (a == "clignoter:0") + { + clignoter.setValue(false); + + props.sendAllData(); // all data change, we don't have to be selctive then + props.sendDone(a); // acknowledge props command action + } + else + { + // acknowledge omition of the props command + props.sendOmit(a); + } +} + +void updateBrokerAdressFromFile(const char* broker_file, BridgeProps* props) +{ + // broker IP address is stored in Linino file systems and updated with ssh command by Room 2.0 + IPAddress ip; + + Process _process; + _process.begin("cat"); + _process.addParameter(broker_file); // for ssh remotely set broker address + _process.run(); // run the process and wait for its termination + String b; + while (_process.available() > 0) { + char c = _process.read(); + b += c; + } + b.trim(); + + if (ip.fromString(b.c_str())) props->setBrokerIpAddress(ip); +} diff --git a/examples/BlinkOnBridgePubSub/BlinkOnBridgePubSub.ino b/examples/BlinkOnBridgePubSub/BlinkOnBridgePubSub.ino new file mode 100644 index 0000000..71d6c63 --- /dev/null +++ b/examples/BlinkOnBridgePubSub/BlinkOnBridgePubSub.ino @@ -0,0 +1,220 @@ +/* BlinkOnBridgePubSub.ino + MIT License (c) Marie Faure + + Adapt the Blink example (https://www.arduino.cc/en/tutorial/blink) as a + simple MQTT props with PubSubClient. +*/ +#include +#include +#include +#include +#include + +#define BROKER "192.168.1.42" // your MQTT server IP address +#define PROPS_NAME u8"Arduino Contrôleur" // as MQTT client id, should be unique per client for given broker + +// If you're running our Escape Room control software (Room 2.0) you have to respect +// props inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox +// https://live-escape.net/go/room +#define PROPS_INBOX u8"Room/Demoniak/Props/Arduino Contrôleur/inbox" +#define PROPS_OUTBOX u8"Room/Demoniak/Props/Arduino Contrôleur/outbox" + +// Yun can store broker IP address in Linino file systems, and updatred with ssh command +#define YUN_BROKER_FILE "/root/broker" + +void publishAll(); // forward +void publishChanges(); // forward + +class Blinking : public VariableTimedAction { +private: + //this method will be called at your specified interval + unsigned long run() { + //swicth LED if blinking mode + if (blink) { + digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); + } + + //return code of 0 indicates no change to the interval + //if the interval must be changed, then return the new interval + return 0; + } + +public: + bool blink; +}; + +class PublishAllData : public VariableTimedAction { +private: + unsigned long run() { + publishAll(); + return 0; + } +}; + +class PublishChangedData : public VariableTimedAction { +private: + unsigned long run() { + publishChanges(); + return 0; + } +}; + +Blinking blinking; + +bool led_ref; +bool blink_ref; + +BridgeClient _ethClient; +PubSubClient _client(_ethClient); +IPAddress _brokerAddress; + +void callback(char*, byte*, unsigned int); // forward + +// MQTT +unsigned long lastReconnection(0L); +PublishAllData publishAllData; +PublishChangedData publishChangedData; + +void setup() +{ + Bridge.begin(); + + Process p; + p.begin("cat"); + p.addParameter(YUN_BROKER_FILE); + p.run(); // run the process and wait for its termination + String b; + while (p.available() > 0) { + char c = p.read(); + b += c; + } + b.trim(); + + if (!_brokerAddress.fromString(b.c_str())) _brokerAddress.fromString(BROKER); + _client.setServer(_brokerAddress, 1883); + _client.setCallback(callback); + + publishAllData.start(30000); + publishChangedData.start(400); + + blinking.blink = true; + blinking.start(1000); + blink_ref = false; + + pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output + digitalWrite(LED_BUILTIN, HIGH); + led_ref = LOW; + + // At this point, the broker is not connected yet +} + +void loop() +{ + if (_client.connected()) + { + _client.loop(); + } + else if (millis() > lastReconnection) + { + lastReconnection += 5000L; + + if (_client.connect(PROPS_NAME, PROPS_OUTBOX, 2, true, "DISCONNECTED")) + { + _client.publish(PROPS_OUTBOX, "CONNECTED", true); + _client.subscribe(PROPS_INBOX, 1); // max QoS is 1 for PubSubClient subsciption + lastReconnection = 0L; + } + } + + // automation code should be here (nothing to do for this example) + + VariableTimedAction::updateActions(); +} + +void publishAll() +{ + String buf = "DATA"; + + bool led = digitalRead(LED_BUILTIN); + + buf += u8" led=" + (led ? String("1") : String("0")); + led_ref = led; + + buf += u8" clignote=" + (blinking.blink ? String("oui") : String("non")); + blink_ref = blinking.blink; + + _client.publish(PROPS_OUTBOX, buf.c_str()); +} + +void publishChanges() +{ + String buf = "DATA"; + + bool led = digitalRead(LED_BUILTIN); + + if (led != led_ref) + { + buf += u8" led=" + (led ? String("1") : String("0")); + led_ref = led; + } + + if (blinking.blink != blink_ref) + { + buf += u8" clignote=" + (blinking.blink ? String("oui") : String("non")); + blink_ref = blinking.blink; + } + + if (buf.length() > 4) + _client.publish(PROPS_OUTBOX, buf.c_str()); +} + +void publishDone(String a) +{ + a = "DONE " + a; + _client.publish(PROPS_OUTBOX, a.c_str()); +} + +void onInboxMessage(String a) { + + if (a == u8"app:startup") + { + publishAll(); + publishDone(a); + } + else if (a == "clignoter:1") + { + digitalWrite(LED_BUILTIN, HIGH); + blinking.blink = true; + + publishAll(); // all data change, we don't have to be selctive then + publishDone(a); // acknowledge props command action + } + else if (a == "clignoter:0") + { + digitalWrite(LED_BUILTIN, LOW); + blinking.blink = false; + + publishAll(); // all data change, we don't have to be selctive then + publishDone(a); // acknowledge props command action + } + else + { + // acknowledge omition of the props command + _client.publish(PROPS_OUTBOX, String("OMIT " + a).c_str()); + } +} + +void callback(char* topic, byte* payload, unsigned int len) +{ + if (len) + { + char* p = (char*)malloc(len + 1); + memcpy(p, payload, len); + p[len] = '\0'; + if (String(p) == "@PING") + _client.publish(PROPS_OUTBOX, "PONG"); + else + onInboxMessage(p); + free(p); + } +} diff --git a/examples/BlinkOnEthernetProps/BlinkOnEthernetProps.ino b/examples/BlinkOnEthernetProps/BlinkOnEthernetProps.ino new file mode 100644 index 0000000..2926bae --- /dev/null +++ b/examples/BlinkOnEthernetProps/BlinkOnEthernetProps.ino @@ -0,0 +1,113 @@ +/* BlinkOnEthernetProps.ino + MIT License (c) Marie Faure + + Adapt the Blink example (https://www.arduino.cc/en/tutorial/blink) as a + simple MQTT props. Avoid delay() calls (except short ones) in loop() to + ensure CPU for MQTT protocol. Use PropsAction checks instead. + + Copy and change it to build your fist Arduino connected props, you will + only be limited by your imagination. + + Requirements: install ArduinoProps.zip library. +*/ +#include +#include +#include "ArduinoProps.h" + +byte mac[] = { 0xFC, 0xFA, 0xEA, 0x00, 0x00, 0x02 }; //<<< MAKE SURE IT'S UNIQUE IN YOUR NETWORK!!! +String ip = "192.168.1.19"; //<<< ENTER YOUR IP ADDRESS HERE ("" for DHCP) + +// Builtin led is not available with the shield +#undef LED_BUILTIN +#define LED_BUILTIN 8 + +// If you're running our Escape Room control software (Room 2.0) you have to respect +// prpos inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox +// https://live-escape.net/go/room + +EthernetProps props(u8"Arduino Contrôleur", // as MQTT client id, should be unique per client for given broker + u8"Room/Demoniak/Props/Arduino Contrôleur/inbox", + u8"Room/Demoniak/Props/Arduino Contrôleur/outbox", + "192.168.1.42", // your MQTT server IP address + 1883); // your MQTT server port; + +PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true); +PropsDataLogical led(u8"led"); + +void clignote(); // forward +PropsAction clignoteAction = PropsAction(1000, clignote); + +void setup() +{ + // can do more static IP configuration + //Ethernet.setSubnetMask(IPAddress()); + //Ethernet.setGatewayIP(IPAddress()); + //Ethernet.setDnsServerIP(IPAddress()); + + IPAddress ipa; + if (!ipa.fromString(ip)) { + // attempt a DHCP connection: + if (!Ethernet.begin(mac)) { + // if DHCP fails, start with a hard-coded address: + Ethernet.begin(mac, IPAddress(10, 90, 90, 239)); + } + } + else { + Ethernet.begin(mac, ipa); + } + delay(1500); // time for shield stuff + + props.addData(&clignoter); + props.addData(&led); + + props.begin(InboxMessage::run); + + pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output + + // At this point, the broker is not connected yet +} + +void loop() +{ + props.loop(); + + led.setValue(digitalRead(LED_BUILTIN)); // read I/O + + clignoteAction.check(); // do your stuff, don't freeze the loop with delay() calls +} + +void clignote() +{ + if (clignoter.value()) { + led.setValue(!led.value()); + digitalWrite(LED_BUILTIN, led.value() ? HIGH : LOW); + } +} + +void InboxMessage::run(String a) { + + if (a == u8"app:startup") + { + props.sendAllData(); + props.sendDone(a); + } + else if (a == "clignoter:1") + { + clignoter.setValue(true); + + props.sendAllData(); // all data change, we don't have to be selctive then + props.sendDone(a); // acknowledge props command action + } + else if (a == "clignoter:0") + { + clignoter.setValue(false); + + props.sendAllData(); // all data change, we don't have to be selctive then + props.sendDone(a); // acknowledge props command action + } + else + { + // acknowledge omition of the props command + props.sendOmit(a); + } +} diff --git a/examples/BlinkOnWifiProps/BlinkOnWifiProps.ino b/examples/BlinkOnWifiProps/BlinkOnWifiProps.ino new file mode 100644 index 0000000..41c5d20 --- /dev/null +++ b/examples/BlinkOnWifiProps/BlinkOnWifiProps.ino @@ -0,0 +1,105 @@ +/* BlinkOnWifiProps.ino + MIT License (c) Marie Faure + + Adapt the Blink example (https://www.arduino.cc/en/tutorial/blink) as a + simple MQTT props. Avoid delay() calls (except short ones) in loop() to + ensure CPU for MQTT protocol. Use PropsAction checks instead. + + Copy and change it to build your fist Arduino connected props, you will + only be limited by your imagination. + + Requirements: install ArduinoProps.zip library. +*/ +#include "ArduinoProps.h" + +// Setup your WiFi network +const char* ssid = ""; +const char *passphrase = ""; + +// Builtin led is not available with the shield +#undef LED_BUILTIN +#define LED_BUILTIN 8 + +// If you're running our Escape Room control software (Room 2.0) you have to respect +// prpos inbox/outbox syntax Room/[escape room name]/Props/[propsname]/inbox|outbox +// https://live-escape.net/go/room + +WifiProps props(u8"Arduino Contrôleur", // as MQTT client id, should be unique per client for given broker + u8"Room/Demoniak/Props/Arduino Contrôleur/inbox", + u8"Room/Demoniak/Props/Arduino Contrôleur/outbox", + "192.168.1.42", // your MQTT server IP address + 1883); // your MQTT server port; + +PropsDataLogical clignoter(u8"clignote", u8"oui", u8"non", true); +PropsDataLogical led(u8"led"); + +void clignote(); // forward +PropsAction clignoteAction = PropsAction(1000, clignote); + +void setup() +{ + // can do more static IP configuration + //WiFi.config(IPAddress(), IPAddress(), IPAddress()); + //WiFi.config(IPAddress(), IPAddress()); + //WiFi.config(IPAddress()); + + WiFi.begin(ssid, passphrase); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + } + + props.addData(&clignoter); + props.addData(&led); + + props.begin(InboxMessage::run); + + pinMode(LED_BUILTIN, OUTPUT); // initialize digital pin LED_BUILTIN as an output + + // At this point, the broker is not connected yet +} + +void loop() +{ + props.loop(); + + led.setValue(digitalRead(LED_BUILTIN)); // read I/O + + clignoteAction.check(); // do your stuff, don't freeze the loop with delay() calls +} + +void clignote() +{ + if (clignoter.value()) { + led.setValue(!led.value()); + digitalWrite(LED_BUILTIN, led.value() ? HIGH : LOW); + } +} + +void InboxMessage::run(String a) { + + if (a == u8"app:startup") + { + props.sendAllData(); + props.sendDone(a); + } + else if (a == "clignoter:1") + { + clignoter.setValue(true); + + props.sendAllData(); // all data change, we don't have to be selctive then + props.sendDone(a); // acknowledge props command action + } + else if (a == "clignoter:0") + { + clignoter.setValue(false); + + props.sendAllData(); // all data change, we don't have to be selctive then + props.sendDone(a); // acknowledge props command action + } + else + { + // acknowledge omition of the props command + props.sendOmit(a); + } +} diff --git a/install-shot.png b/install-shot.png new file mode 100644 index 0000000000000000000000000000000000000000..7fc5e3f4abb1de09d044aa164cd6d105d95d8a7f GIT binary patch literal 27378 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 vAjH*Ml>=fYBPWR;ADoB5qGz$1%c-sqp literal 0 HcmV?d00001 diff --git a/keywords.txt b/keywords.txt new file mode 100644 index 0000000..f122c3f --- /dev/null +++ b/keywords.txt @@ -0,0 +1,59 @@ +####################################### +# Syntax Coloring Map For PubSubClient +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +Props KEYWORD1 +BridgeProps KEYWORD1 +EthernetProps KEYWORD1 +WifiProps KEYWORD1 +PropsAction KEYWORD1 +PropsData KEYWORD1 +PropsDataDecimal KEYWORD1 +PropsDataInteger KEYWORD1 +PropsDataLogical KEYWORD1 +PropsDataText KEYWORD1 +PropsCallback KEYWORD1 +PropsMessageReceived KEYWORD1 +InboxMessage KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +begin KEYWORD2 +addData KEYWORD2 +loop KEYWORD2 +run KEYWORD2 +sendAllData KEYWORD2 +sendDataChanges KEYWORD2 +sendData KEYWORD2 +sendDone KEYWORD2 +sendMesg KEYWORD2 +sendOmit KEYWORD2 +sendOver KEYWORD2 +sendProg KEYWORD2 +sendRequ KEYWORD2 +reset KEYWORD2 +disable KEYWORD2 +enable KEYWORD2 +check KEYWORD2 +tick KEYWORD2 +getInterval KEYWORD2 +fetch KEYWORD2 +fetchChange KEYWORD2 +setValue KEYWORD2 +value KEYWORD2 +onInboxMessageReceived KEYWORD2 +setBrokerIpAddress KEYWORD2 +checkDataChanges KEYWORD2 +resetIntervals KEYWORD2 +send KEYWORD2 + + +####################################### +# Constants (LITERAL1) +####################################### diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..0bab369 --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=ArduinoProps +version=1.0.0 +author=Marie Faure +maintainer=Marie Faure +sentence=ArduinoProps Library +paragraph= +category=Communication +url=https://github.com/fauresystems +architectures=* \ No newline at end of file diff --git a/src/ArduinoProps.cpp b/src/ArduinoProps.cpp new file mode 100644 index 0000000..3049d30 --- /dev/null +++ b/src/ArduinoProps.cpp @@ -0,0 +1,11 @@ +/* + Name: ArduinoProps.h + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Library to make props sketch for Escape Room 2.0 (connected props). +*/ + +#include "ArduinoProps.h" diff --git a/src/ArduinoProps.h b/src/ArduinoProps.h new file mode 100644 index 0000000..3a85481 --- /dev/null +++ b/src/ArduinoProps.h @@ -0,0 +1,26 @@ +/* + Name: ArduinoProps.h + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Library to make props sketch for Escape Room 2.0 (connected props). +*/ + +#ifndef _ArduinoProps_h +#define _ArduinoProps_h + +#if defined(ARDUINO) && ARDUINO >= 100 +#include "arduino.h" +#else +#include "WProgram.h" +#endif + +#include "BridgeProps.h" +#include "EthernetProps.h" +#include "WifiProps.h" +#include "PropsAction.h" +#include "PropsData.h" + +#endif diff --git a/src/BridgeProps.cpp b/src/BridgeProps.cpp new file mode 100644 index 0000000..a4b4854 --- /dev/null +++ b/src/BridgeProps.cpp @@ -0,0 +1,28 @@ +/* + Name: BridgeProps.cpp + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Class Props for Arduino Yun and Dragino Yun shield. +*/ + +#include "BridgeProps.h" + +BridgeProps::BridgeProps(const char* client_id, const char* in_box, const char* out_box, const char* broker, const int port) + : Props(client_id, in_box, out_box, broker, port) +{ + _client.setClient(_bridgeClient); +} + +void BridgeProps::begin(void(*on_message)(String)) +{ + if (on_message) onInboxMessageReceived = on_message; +} + +void BridgeProps::setBrokerIpAddress(IPAddress ip) +{ + _brokerIpAddress = ip; +} + diff --git a/src/BridgeProps.h b/src/BridgeProps.h new file mode 100644 index 0000000..7e55a7f --- /dev/null +++ b/src/BridgeProps.h @@ -0,0 +1,28 @@ +/* + Name: BridgeProps.h + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Class Props for Arduino Yun and Dragino Yun shield. +*/ +#ifndef BRIDGEPROPS_H +#define BRIDGEPROPS_H + +#include +#include +#include "Props.h" + +class BridgeProps : public Props +{ + public: + BridgeProps(const char*, const char*, const char*, const char*, const int); + void begin(void(*)(String) = NULL); + void setBrokerIpAddress(IPAddress); + + private: + BridgeClient _bridgeClient; +}; + +#endif diff --git a/src/EthernetProps.cpp b/src/EthernetProps.cpp new file mode 100644 index 0000000..bbf2b7a --- /dev/null +++ b/src/EthernetProps.cpp @@ -0,0 +1,22 @@ +/* + Name: EthernetProps.cpp + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Class Props for Arduino with Ethernet shield. +*/ + +#include "EthernetProps.h" + +EthernetProps::EthernetProps(const char* client_id, const char* in_box, const char* out_box, const char* broker, const int port) + : Props(client_id, in_box, out_box, broker, port) +{ + _client.setClient(_ethernetClient); +} + +void EthernetProps::begin(void(*on_message)(String)) +{ + if (on_message) onInboxMessageReceived = on_message; +} diff --git a/src/EthernetProps.h b/src/EthernetProps.h new file mode 100644 index 0000000..332c8d6 --- /dev/null +++ b/src/EthernetProps.h @@ -0,0 +1,29 @@ +/* + Name: EthernetProps.h + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Class Props for Arduino with Ethernet shield. +*/ +#ifndef ETHERNETPROPS_H +#define ETHERNETPROPS_H + + +#include +#include +#include +#include "Props.h" + +class EthernetProps : public Props +{ + public: + EthernetProps(const char*, const char*, const char*, const char*, const int); + void begin(void(*)(String) = NULL); + + private: + EthernetClient _ethernetClient; +}; + +#endif diff --git a/src/Props.cpp b/src/Props.cpp new file mode 100644 index 0000000..748669b --- /dev/null +++ b/src/Props.cpp @@ -0,0 +1,191 @@ +/* + Name: Props.cpp + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Base class to make props sketch for Escape Room 2.0 (connected). +*/ + +#include "Props.h" +#include + +void PropsCallback::run(char* topic, byte* payload, unsigned int len) +{ + if (len) + { + char* p = (char*)malloc(len + 1); + memcpy(p, payload, len); + p[len] = '\0'; + if (String(p) == "@PING") + client->publish(outbox, "PONG"); + else + Props::onInboxMessageReceived(p); + free(p); + } +} + +void PropsMessageReceived::run(String a) +{ + client->publish(outbox, String("OMIT " + a).c_str()); +} + +Props::Props(const char* client_id, const char* in_box, const char* out_box, const char* broker, const int port) +{ + clientId = client_id; + inbox = in_box; + outbox = out_box; + + _nextReconAttempt = 0; + _dataSentCount = 0; + _maximumSilentPeriod = 30; // 30 seconds + _payloadMax = MQTT_MAX_PACKET_SIZE - (1 + 2 + strlen(Props::outbox)); + + _brokerIpAddress.fromString(broker); + _client.setServer(_brokerIpAddress, port); + + PropsCallback::client = &_client; + PropsCallback::outbox = outbox; + _client.setCallback(PropsCallback::run); + + PropsMessageReceived::client = &_client; + PropsMessageReceived::outbox = outbox; + + _sendDataAction.reset(400); // check data changes every 400 milliseconds +} + +void Props::addData(PropsData* d) +{ + _dataTable.push_back(d); +} + +void Props::loop() +{ + if (_client.connected()) + { + _client.loop(); + } + else if (millis() > _nextReconAttempt) + { + _nextReconAttempt += 5000L; + + if (_client.connect(clientId, outbox, 2, true, "DISCONNECTED")) + { + _client.publish(outbox, "CONNECTED", true); + _client.subscribe(inbox, 1); // max QoS is 1 for PubSubClient subsciption + _nextReconAttempt = 0L; + } + } + + if (_sendDataAction.tick()) checkDataChanges(); // send data changes if any +} + +void Props::checkDataChanges() +{ + if (_dataSentCount) { + sendDataChanges(); + } else { + sendAllData(); + } + + ++_dataSentCount; + + // don't be silent to long + if (_dataSentCount > (_maximumSilentPeriod * 1000 / _sendDataAction.getInterval())) _dataSentCount = 0; +} + +void Props::sendAllData() +{ + String data("DATA "), str; + for (SimpleList::iterator itr = _dataTable.begin(); itr != _dataTable.end(); ++itr) { + str = (*itr)->fetch(); + send(&data, &str); + } + + if (data.length() > 5) { + _client.publish(outbox, data.c_str()); + } +} + +void Props::sendDataChanges() +{ + String data("DATA "), str; + for (SimpleList::iterator itr = _dataTable.begin(); itr != _dataTable.end(); ++itr) { + str = (*itr)->fetchChange(); + send(&data, &str); + } + + if (data.length() > 5) { + _client.publish(outbox, data.c_str()); + } +} + +void Props::send(String* d, String* s) +{ + if (d->length() + s->length() <= _payloadMax) { + *d += *s; + } else if (s->length() > _payloadMax + 5) { + sendMesg(u8"Data ignored, loo larged to be sent"); + } else if (d->length() + s->length() > _payloadMax) { + _client.publish(outbox, d->c_str()); + *d = "DATA " + *s; + } +} + +void Props::sendData(String data) { + if (data.length() > 5) { + _client.publish(outbox, String("DATA " + data).c_str()); + } +} + +void Props::sendDone(String action) { + _client.publish(outbox, String("DONE " + action).c_str()); +} + +void Props::sendMesg(String mesg) { + if (mesg.length() > 5) { + _client.publish(outbox, String("MESG " + mesg).c_str()); + } +} + +void Props::sendMesg(String mesg, char *topic) { + if (mesg.length() > 5) { + _client.publish(topic, String("MESG " + mesg).c_str()); + } +} + +void Props::sendOmit(String action) { + _client.publish(outbox, String("OMIT " + action).c_str()); +} + +void Props::sendOver(String challenge) { + _client.publish(outbox, String("OMIT " + challenge).c_str()); +} + +void Props::sendProg(String program) { + _client.publish(outbox, String("PROGRAM " + program).c_str()); +} + +void Props::sendRequ(String request) { + _client.publish(outbox, String("REQU " + request).c_str()); +} + +void Props::sendRequ(String action, char* topic) { + _client.publish(topic, String("OMIT " + action).c_str()); +} + +void Props::resetIntervals(const int changes, const int silent) +{ + _nextReconAttempt = 0; + _dataSentCount = 0; + _maximumSilentPeriod = silent; // seconds + _sendDataAction.reset(changes); // milliseconds +} + + +PubSubClient *PropsCallback::client; +const char *PropsCallback::outbox; +PubSubClient *PropsMessageReceived::client; +const char *PropsMessageReceived::outbox; +void (*Props::onInboxMessageReceived)(String) = PropsMessageReceived::run; diff --git a/src/Props.h b/src/Props.h new file mode 100644 index 0000000..97a4c05 --- /dev/null +++ b/src/Props.h @@ -0,0 +1,92 @@ +/* + Name: Props.h + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Base class to make props sketch for Escape Room 2.0 (connected). +*/ +#ifndef PROPS_H +#define PROPS_H + +#if defined(ARDUINO) && ARDUINO >= 100 +#include "arduino.h" +#else +#include "WProgram.h" +#endif + +#include +#include +#include +#include "PropsAction.h" +#include "PropsData.h" + +#ifndef MQTT_MAX_PACKET_SIZE +#define MQTT_MAX_PACKET_SIZE 128 // Bridge tranfer stuff (see PubSubClient) +#endif + +class PropsCallback +{ + public: + static PubSubClient *client; + static const char *outbox; + static void run(char* topic, byte* payload, unsigned int len); +}; + +class PropsMessageReceived +{ + public: + static PubSubClient *client; + static const char *outbox; + static void run(String); +}; + +class InboxMessage : public PropsMessageReceived +{ + public: + static void run(String); // to be implemented in .ino sketch +}; + +class Props +{ + public: + Props(const char*, const char*, const char*, const char*, const int=1883); + virtual void begin(void(*)(String) = NULL) = 0; + void addData(PropsData*); + void loop(); + void resetIntervals(const int changes, const int silent); + void sendAllData(); + void sendDataChanges(); + void sendData(String); // only in outbox + void sendDone(String); + void sendMesg(String); + void sendMesg(String, char*); + void sendOmit(String); + void sendOver(String); + void sendProg(String); + void sendRequ(String); + void sendRequ(String, char*); + + static void (*onInboxMessageReceived)(String); + + const char *clientId; + const char *inbox; + const char *outbox; + + protected: + void checkDataChanges(); + void send(String*, String*); + + IPAddress _brokerIpAddress; + PubSubClient _client; + + uint16_t _dataSentCount; + uint8_t _maximumSilentPeriod; + unsigned long _nextReconAttempt; + uint8_t _payloadMax; + PropsAction _sendDataAction; + SimpleList _dataTable; +}; + +#endif diff --git a/src/PropsAction.cpp b/src/PropsAction.cpp new file mode 100644 index 0000000..564f646 --- /dev/null +++ b/src/PropsAction.cpp @@ -0,0 +1,66 @@ +/* + Name: PropsAction.h + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Provide an easy way of triggering functions at a set _interval. +*/ + +#include "PropsAction.h" + +PropsAction::PropsAction() +{ + _active = true; + _previous = 0; + _interval = 400; + _execute = NULL; +} + +PropsAction::PropsAction(unsigned long intervl, void (*function)()) +{ + _active = true; + _previous = 0; + _interval = intervl; + _execute = function; +} + +void PropsAction::reset(unsigned long intervl) +{ + _active = true; + _previous = 0; + _interval = intervl; +} + +void PropsAction::disable() +{ + _active = false; +} + +void PropsAction::enable() +{ + _active = true; +} + +void PropsAction::check() { + if (_active && (millis() - _previous >= _interval)) + { + _previous = millis(); + if (_execute) _execute(); + } +} + +bool PropsAction::tick() { + if (_active && (millis() - _previous >= _interval)) + { + _previous = millis(); + return true; + } + return false; +} + +unsigned long PropsAction::getInterval() +{ + return _interval; +} diff --git a/src/PropsAction.h b/src/PropsAction.h new file mode 100644 index 0000000..ee6a0b3 --- /dev/null +++ b/src/PropsAction.h @@ -0,0 +1,43 @@ +/* + Name: PropsAction.h + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Provide an easy way of triggering functions at a set interval. +*/ + +#ifndef PROPSACTION_H +#define PROPSACTION_H + +#if defined(ARDUINO) && ARDUINO >= 100 +#include "arduino.h" +#else +#include "WProgram.h" +#endif + +class Props; + +class PropsAction { + + public: + PropsAction(); + PropsAction(unsigned long interval, void (*function)()); + + void reset(unsigned long interval); + void disable(); + void enable(); + void check(); + bool tick(); + + unsigned long getInterval(); + + private: + bool _active; + unsigned long _previous; + unsigned long _interval; + void (*_execute)(); +}; + +#endif diff --git a/src/PropsData.cpp b/src/PropsData.cpp new file mode 100644 index 0000000..a8356f8 --- /dev/null +++ b/src/PropsData.cpp @@ -0,0 +1,136 @@ +/* + Name: PropsData.cpp + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Variable wrapper for connected props. +*/ + +#include "PropsData.h" +#include + +// ---- Decimal +PropsDataDecimal::PropsDataDecimal(const char * id, uint8_t digits, double precision, double initial) { + _id = id; + _current = initial; + _previous = _current; + _precision = precision; + _digits = digits; +} + +String PropsDataDecimal::fetch() { + _previous = _current; + return String(_id) + "=" + String(_current, _digits) + " "; +} + +String PropsDataDecimal::fetchChange() { + if (_current != _previous && _current && abs(_current - _previous) / _current >= _precision) { + return fetch(); + } + else if (_current != _previous && !_current && abs(_current - _previous) >= _precision) { + return fetch(); + } + return String(); +} + +void PropsDataDecimal::setValue(const double v) { + _current = v; +} + +double PropsDataDecimal::value() const { + return _current; +} + +// ---- Integer +PropsDataInteger::PropsDataInteger(const char * id, double precision, long initial) { + _id = id; + _current = initial; + _previous = _current; + _precision = precision; +} + +String PropsDataInteger::fetch() { + _previous = _current; + return String(_id) + "=" + String(_current) + " "; +} + +String PropsDataInteger::fetchChange() { + if (_current != _previous && _current && abs(_current - _previous) / _current >= _precision) { + return fetch(); + } + else if (_current != _previous && !_current && abs(_current - _previous) >= _precision) { + return fetch(); + } + return String(); +} + +void PropsDataInteger::setValue(const long v) { + _current = v; +} + +long PropsDataInteger::value() const { + return _current; +} + +// ---- Logical +PropsDataLogical::PropsDataLogical(const char * id, const char *trueval, const char *falseval, bool initial) { + _id = id; + _current = initial; + _previous = _current; + _trueString = trueval; + _falseString = falseval; +} + +String PropsDataLogical::fetch() { + String str(String(_id) + "="); + if (_current) { + str += _trueString ? _trueString : "1"; + } else { + str += _falseString ? _falseString : "0"; + } + str += " "; + _previous = _current; + return str; +} + +String PropsDataLogical::fetchChange() { + if (_current != _previous) { + return fetch(); + } + return String(); +} + +void PropsDataLogical::setValue(const bool v) { + _current = v; +} + +bool PropsDataLogical::value() const { + return _current; +} + +// ---- Text +PropsDataText::PropsDataText(const char * id) { + _id = id; +} + +String PropsDataText::fetch() { + _previous = _current; + return String(_id) + "=" + _current + " "; +} + +String PropsDataText::fetchChange() { + if (_current != _previous) { + return fetch(); + } + return String(); +} + +void PropsDataText::setValue(String v) { + _current = v; +} + +String PropsDataText::value() const { + return _current; +} diff --git a/src/PropsData.h b/src/PropsData.h new file mode 100644 index 0000000..13ec1cf --- /dev/null +++ b/src/PropsData.h @@ -0,0 +1,90 @@ +/* + Name: PropsData.h + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Variable wrapper for connected props. +*/ +#ifndef PROPSDATA_H +#define PROPSDATA_H + +#if defined(ARDUINO) && ARDUINO >= 100 +#include "arduino.h" +#else +#include "WProgram.h" +#endif + +#include +#include + +class PropsData +{ + public: + virtual String fetch() = 0; + virtual String fetchChange() = 0; +}; + +class PropsDataDecimal : public PropsData +{ + public: + PropsDataDecimal(const char *, uint8_t digits = 2, double precision = 0.1, double initial = 0); + String fetch(); + String fetchChange(); + void setValue(const double); + double value() const; + private: + const char *_id; + double _current; + double _previous; + double _precision; + uint8_t _digits; +}; + +class PropsDataInteger : public PropsData +{ + public: + PropsDataInteger(const char *, double precision = 0.1, long initial = 0); + String fetch(); + String fetchChange(); + void setValue(const long); + long value() const; + private: + const char *_id; + long _current; + long _previous; + double _precision; +}; + +class PropsDataLogical : public PropsData +{ + public: + PropsDataLogical(const char *, const char *trueval = NULL, const char *falseval = NULL, bool initial = false); + String fetch(); + String fetchChange(); + void setValue(const bool); + bool value() const; + private: + const char *_id; + bool _current; + bool _previous; + const char *_trueString; + const char *_falseString; +}; + +class PropsDataText : public PropsData +{ + public: + PropsDataText(const char *); + String fetch(); + String fetchChange(); + void setValue(String); + String value() const; + private: + const char *_id; + String _current; + String _previous; +}; + +#endif diff --git a/src/WifiProps.cpp b/src/WifiProps.cpp new file mode 100644 index 0000000..6d98452 --- /dev/null +++ b/src/WifiProps.cpp @@ -0,0 +1,29 @@ +/* + Name: WifiProps.cpp + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Class Props for Arduino with Wifi shield. +*/ + +#include "WifiProps.h" + + +#if defined(ARDUINO_AVR_UNO_WIFI_REV2) || defined(ARDUINO_SAMD_MKRWIFI1010) || defined(ARDUINO_SAMD_NANO_33_IOT) || defined(ARDUINO_SAMD_MKRVIDOR4000) + + +WifiProps::WifiProps(const char* client_id, const char* in_box, const char* out_box, const char* broker, const int port) + : Props(client_id, in_box, out_box, broker, port) +{ + _client.setClient(_wifiClient); +} + +void WifiProps::begin(void(*on_message)(String)) +{ + if (on_message) onInboxMessageReceived = on_message; +} + + +#endif diff --git a/src/WifiProps.h b/src/WifiProps.h new file mode 100644 index 0000000..b0ccb4a --- /dev/null +++ b/src/WifiProps.h @@ -0,0 +1,36 @@ +/* + Name: WifiProps.h + Created: 29/10/2019 09:20:31 + Author: Marie Faure + Editor: https://github.com/fauresystems + License: MIT License (c) Marie Faure + + Class Props for Arduino with Wifi shield. +*/ +#ifndef WIFIPROPS_H +#define WIFIPROPS_H + + +#if defined(ARDUINO_AVR_UNO_WIFI_REV2) || defined(ARDUINO_SAMD_MKRWIFI1010) || defined(ARDUINO_SAMD_NANO_33_IOT) || defined(ARDUINO_SAMD_MKRVIDOR4000) + + +#include + +#include +#include +#include "Props.h" + +class WifiProps : public Props +{ + public: + WifiProps(const char*, const char*, const char*, const char*, const int); + void begin(void(*)(String) = NULL); + +private: + WiFiClient _wifiClient; +}; + +#endif + + +#endif