Преглед на файлове

Add ability to select which channels sends are sent on (#421)

* Add ability to select channels messages are sent on for RF24
* Add ability to select listen channel for RF24
* Add test for selectable sends/listens
Chris Mullins преди 6 години
родител
ревизия
4b0ac7890f

Файловите разлики са ограничени, защото са твърде много
+ 2 - 2
dist/index.html.gz.h


+ 36 - 0
lib/Helpers/JsonHelpers.h

@@ -0,0 +1,36 @@
+#include <ArduinoJson.h>
+#include <vector>
+#include <functional>
+#include <algorithm>
+
+#ifndef _JSON_HELPERS_H
+#define _JSON_HELPERS_H
+
+class JsonHelpers {
+public:
+  template<typename T>
+  static std::vector<T> jsonArrToVector(JsonArray& arr, std::function<T (const String&)> converter, const bool unique = true) {
+    std::vector<T> vec;
+
+    for (size_t i = 0; i < arr.size(); ++i) {
+      String strVal = arr.get<const char*>(i);
+      T convertedVal = converter(strVal);
+
+      // inefficient, but everything using this is tiny, so doesn't matter
+      if (!unique || std::find(vec.begin(), vec.end(), convertedVal) == vec.end()) {
+        vec.push_back(convertedVal);
+      }
+    }
+
+    return vec;
+  }
+
+  template<typename T>
+  static void vectorToJsonArr(JsonArray& arr, const std::vector<T>& vec, std::function<String (const T&)> converter) {
+    for (typename std::vector<T>::const_iterator it = vec.begin(); it != vec.end(); ++it) {
+      arr.add(converter(*it));
+    }
+  }
+};
+
+#endif

+ 18 - 4
lib/Radio/MiLightRadioFactory.cpp

@@ -3,7 +3,13 @@
 MiLightRadioFactory* MiLightRadioFactory::fromSettings(const Settings& settings) {
   switch (settings.radioInterfaceType) {
     case nRF24:
-      return new NRF24Factory(settings.csnPin, settings.cePin, settings.rf24PowerLevel);
+      return new NRF24Factory(
+        settings.csnPin, 
+        settings.cePin, 
+        settings.rf24PowerLevel, 
+        settings.rf24Channels,
+        settings.rf24ListenChannel
+      );
 
     case LT8900:
       return new LT8900Factory(settings.csnPin, settings.resetPin, settings.cePin);
@@ -13,14 +19,22 @@ MiLightRadioFactory* MiLightRadioFactory::fromSettings(const Settings& settings)
   }
 }
 
-NRF24Factory::NRF24Factory(uint8_t csnPin, uint8_t cePin, RF24PowerLevel rF24PowerLevel)
-  : rf24(RF24(cePin, csnPin))
+NRF24Factory::NRF24Factory(
+  uint8_t csnPin, 
+  uint8_t cePin, 
+  RF24PowerLevel rF24PowerLevel, 
+  const std::vector<RF24Channel>& channels,
+  RF24Channel listenChannel
+)
+: rf24(RF24(cePin, csnPin)),
+  channels(channels),
+  listenChannel(listenChannel)
 { 
   rf24.setPALevel(RF24PowerLevelHelpers::rf24ValueFromValue(rF24PowerLevel));
 }
 
 MiLightRadio* NRF24Factory::create(const MiLightRadioConfig &config) {
-  return new NRF24MiLightRadio(rf24, config);
+  return new NRF24MiLightRadio(rf24, config, channels, listenChannel);
 }
 
 LT8900Factory::LT8900Factory(uint8_t csPin, uint8_t resetPin, uint8_t pktFlag)

+ 11 - 1
lib/Radio/MiLightRadioFactory.h

@@ -5,7 +5,9 @@
 #include <NRF24MiLightRadio.h>
 #include <LT8900MiLightRadio.h>
 #include <RF24PowerLevel.h>
+#include <RF24Channel.h>
 #include <Settings.h>
+#include <vector>
 
 #ifndef _MILIGHT_RADIO_FACTORY_H
 #define _MILIGHT_RADIO_FACTORY_H
@@ -22,13 +24,21 @@ public:
 class NRF24Factory : public MiLightRadioFactory {
 public:
 
-  NRF24Factory(uint8_t cePin, uint8_t csnPin, RF24PowerLevel rF24PowerLevel);
+  NRF24Factory(
+    uint8_t cePin, 
+    uint8_t csnPin, 
+    RF24PowerLevel rF24PowerLevel, 
+    const std::vector<RF24Channel>& channels,
+    RF24Channel listenChannel
+  );
 
   virtual MiLightRadio* create(const MiLightRadioConfig& config);
 
 protected:
 
   RF24 rf24;
+  const std::vector<RF24Channel>& channels;
+  const RF24Channel listenChannel;
 
 };
 

+ 15 - 4
lib/Radio/NRF24MiLightRadio.cpp

@@ -5,8 +5,15 @@
 
 #define PACKET_ID(packet, packet_length) ( (packet[1] << 8) | packet[packet_length - 1] )
 
-NRF24MiLightRadio::NRF24MiLightRadio(RF24& rf24, const MiLightRadioConfig& config)
+NRF24MiLightRadio::NRF24MiLightRadio(
+  RF24& rf24, 
+  const MiLightRadioConfig& config, 
+  const std::vector<RF24Channel>& channels,
+  RF24Channel listenChannel
+)
   : _pl1167(PL1167_nRF24(rf24)),
+    channels(channels),
+    listenChannelIx(static_cast<size_t>(listenChannel)),
     _waiting(false),
     _config(config)
 { }
@@ -65,7 +72,7 @@ bool NRF24MiLightRadio::available() {
     return true;
   }
 
-  if (_pl1167.receive(_config.channels[0]) > 0) {
+  if (_pl1167.receive(_config.channels[listenChannelIx]) > 0) {
 #ifdef DEBUG_PRINTF
   printf("NRF24MiLightRadio - received packet!\n");
 #endif
@@ -131,10 +138,14 @@ int NRF24MiLightRadio::write(uint8_t frame[], size_t frame_length) {
 }
 
 int NRF24MiLightRadio::resend() {
-  for (size_t i = 0; i < MiLightRadioConfig::NUM_CHANNELS; i++) {
+  for (std::vector<RF24Channel>::const_iterator it = channels.begin(); it != channels.end(); ++it) {
+    size_t channelIx = static_cast<uint8_t>(*it);
+    uint8_t channel = _config.channels[channelIx];
+
     _pl1167.writeFIFO(_out_packet, _out_packet[0] + 1);
-    _pl1167.transmit(_config.channels[i]);
+    _pl1167.transmit(channel);
   }
+
   return 0;
 }
 

+ 11 - 1
lib/Radio/NRF24MiLightRadio.h

@@ -10,13 +10,20 @@
 #include <PL1167_nRF24.h>
 #include <MiLightRadioConfig.h>
 #include <MiLightRadio.h>
+#include <RF24Channel.h>
+#include <vector>
 
 #ifndef _NRF24_MILIGHT_RADIO_H_
 #define _NRF24_MILIGHT_RADIO_H_
 
 class NRF24MiLightRadio : public MiLightRadio {
   public:
-    NRF24MiLightRadio(RF24& rf, const MiLightRadioConfig& config);
+    NRF24MiLightRadio(
+      RF24& rf, 
+      const MiLightRadioConfig& config, 
+      const std::vector<RF24Channel>& channels, 
+      RF24Channel listenChannel
+    );
 
     int begin();
     bool available();
@@ -28,6 +35,9 @@ class NRF24MiLightRadio : public MiLightRadio {
     const MiLightRadioConfig& config();
 
   private:
+    const std::vector<RF24Channel>& channels;
+    const size_t listenChannelIx;
+
     PL1167_nRF24 _pl1167;
     const MiLightRadioConfig& _config;
     uint32_t _prev_packet_id;

+ 15 - 0
lib/Settings/Settings.cpp

@@ -3,6 +3,7 @@
 #include <FS.h>
 #include <IntParsing.h>
 #include <algorithm>
+#include <JsonHelpers.h>
 
 #define PORT_POSITION(s) ( s.indexOf(':') )
 
@@ -110,6 +111,15 @@ void Settings::patch(JsonObject& parsedSettings) {
     this->setIfPresent(parsedSettings, "led_mode_packet_count", ledModePacketCount);
     this->setIfPresent(parsedSettings, "hostname", hostname);
 
+    if (parsedSettings.containsKey("rf24_channels")) {
+      JsonArray& arr = parsedSettings["rf24_channels"];
+      rf24Channels = JsonHelpers::jsonArrToVector<RF24Channel>(arr, RF24ChannelHelpers::valueFromName);
+    }
+
+    if (parsedSettings.containsKey("rf24_listen_channel")) {
+      this->rf24ListenChannel = RF24ChannelHelpers::valueFromName(parsedSettings["rf24_listen_channel"]);
+    }
+
     if (parsedSettings.containsKey("rf24_power_level")) {
       this->rf24PowerLevel = RF24PowerLevelHelpers::valueFromName(parsedSettings["rf24_power_level"]);
     }
@@ -216,6 +226,11 @@ void Settings::serialize(Stream& stream, const bool prettyPrint) {
   root["led_mode_packet_count"] = this->ledModePacketCount;
   root["hostname"] = this->hostname;
   root["rf24_power_level"] = RF24PowerLevelHelpers::nameFromValue(this->rf24PowerLevel);
+  root["rf24_listen_channel"] = RF24ChannelHelpers::nameFromValue(rf24ListenChannel);
+
+  JsonArray& channelArr = jsonBuffer.createArray();
+  JsonHelpers::vectorToJsonArr<RF24Channel>(channelArr, rf24Channels, RF24ChannelHelpers::nameFromValue);
+  root["rf24_channels"] = channelArr;
 
   if (this->deviceIds) {
     JsonArray& arr = jsonBuffer.createArray();

+ 8 - 1
lib/Settings/Settings.h

@@ -3,8 +3,10 @@
 #include <ArduinoJson.h>
 #include <GroupStateField.h>
 #include <RF24PowerLevel.h>
+#include <RF24Channel.h>
 #include <Size.h>
 #include <LEDStatus.h>
+#include <vector>
 
 #ifndef _SETTINGS_H_INCLUDED
 #define _SETTINGS_H_INCLUDED
@@ -101,7 +103,9 @@ public:
     ledModePacket(LEDStatus::LEDMode::Flicker),
     ledModePacketCount(3),
     hostname("milight-hub"),
-    rf24PowerLevel(RF24PowerLevelHelpers::defaultValue())
+    rf24PowerLevel(RF24PowerLevelHelpers::defaultValue()),
+    rf24Channels(RF24ChannelHelpers::allValues()),
+    rf24ListenChannel(RF24Channel::RF24_LOW)
   {
     if (groupStateFields == NULL) {
       numGroupStateFields = size(DEFAULT_GROUP_STATE_FIELDS);
@@ -125,6 +129,7 @@ public:
 
   static RadioInterfaceType typeFromString(const String& s);
   static String typeToString(RadioInterfaceType type);
+  static std::vector<RF24Channel> defaultListenChannels();
 
   void save();
   String toJson(const bool prettyPrint = true);
@@ -174,6 +179,8 @@ public:
   size_t ledModePacketCount;
   String hostname;
   RF24PowerLevel rf24PowerLevel;
+  std::vector<RF24Channel> rf24Channels;
+  RF24Channel rf24ListenChannel;
 
 
 protected:

+ 39 - 0
lib/Types/RF24Channel.cpp

@@ -0,0 +1,39 @@
+#include <Size.h>
+#include <RF24Channel.h>
+
+String RF24ChannelHelpers::nameFromValue(const RF24Channel& value) {
+  const size_t ix = static_cast<size_t>(value);
+
+  if (ix >= size(RF24_CHANNEL_NAMES)) {
+    Serial.println(F("ERROR: unknown RF24 channel label - this is a bug!"));
+    return nameFromValue(defaultValue());
+  }
+
+  return RF24_CHANNEL_NAMES[ix];
+}
+
+RF24Channel RF24ChannelHelpers::valueFromName(const String& name) {
+  for (size_t i = 0; i < size(RF24_CHANNEL_NAMES); ++i) {
+    if (name == RF24_CHANNEL_NAMES[i]) {
+      return static_cast<RF24Channel>(i);
+    }
+  }
+
+  Serial.printf_P(PSTR("WARN: tried to fetch unknown RF24 channel: %s, using default.\n"), name.c_str());
+
+  return defaultValue();
+}
+
+RF24Channel RF24ChannelHelpers::defaultValue() {
+  return RF24Channel::RF24_HIGH;
+}
+
+std::vector<RF24Channel> RF24ChannelHelpers::allValues() {
+  std::vector<RF24Channel> vec;
+
+  for (size_t i = 0; i < size(RF24_CHANNEL_NAMES); ++i) {
+    vec.push_back(valueFromName(RF24_CHANNEL_NAMES[i]));
+  }
+
+  return vec;
+}

+ 27 - 0
lib/Types/RF24Channel.h

@@ -0,0 +1,27 @@
+#include <Arduino.h>
+#include <vector>
+
+#ifndef _RF24_CHANNELS_H
+#define _RF24_CHANNELS_H
+
+static const char* RF24_CHANNEL_NAMES[] = {
+  "LOW",
+  "MID",
+  "HIGH"
+};
+
+enum class RF24Channel {
+  RF24_LOW = 0,
+  RF24_MID = 1,
+  RF24_HIGH = 2
+};
+
+class RF24ChannelHelpers {
+public:
+  static String nameFromValue(const RF24Channel& value);
+  static RF24Channel valueFromName(const String& name);
+  static RF24Channel defaultValue();
+  static std::vector<RF24Channel> allValues();
+};
+
+#endif

+ 1 - 0
test/remote/lib/api_client.rb

@@ -23,6 +23,7 @@ class ApiClient
       req = req_type.new(uri)
       if req_body
         req['Content-Type'] = 'application/json'
+        req_body = req_body.to_json if !req_body.is_a?(String)
         req.body = req_body
       end
 

+ 37 - 0
test/remote/spec/settings_spec.rb

@@ -0,0 +1,37 @@
+require 'api_client'
+
+RSpec.describe 'Settings' do
+  before(:all) do
+    @client = ApiClient.new(ENV.fetch('ESPMH_HOSTNAME'), ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE'))
+    @client.upload_json('/settings', 'settings.json')
+  end
+
+  context 'radio' do
+    it 'should store a set of channels' do
+      val = %w(HIGH LOW)
+      @client.put('/settings', rf24_channels: val)
+      result = @client.get('/settings')
+      expect(result['rf24_channels']).to eq(val)
+
+      val = %w(MID LOW)
+      @client.put('/settings', rf24_channels: val)
+      result = @client.get('/settings')
+      expect(result['rf24_channels']).to eq(val)
+
+      val = %w(MID LOW LOW LOW)
+      @client.put('/settings', rf24_channels: val)
+      result = @client.get('/settings')
+      expect(result['rf24_channels']).to eq(Set.new(val).to_a)
+    end
+
+    it 'should store a listen channel' do
+      @client.put('/settings', rf24_listen_channel: 'MID')
+      result = @client.get('/settings')
+      expect(result['rf24_listen_channel']).to eq('MID')
+
+      @client.put('/settings', rf24_listen_channel: 'LOW')
+      result = @client.get('/settings')
+      expect(result['rf24_listen_channel']).to eq('LOW')
+    end
+  end
+end

+ 49 - 9
web/src/js/script.js

@@ -161,6 +161,31 @@ var UI_FIELDS = [ {
     },
     tab: "tab-radio"
   }, {
+    tag:   "rf24_listen_channel", 
+    friendly: "nRF24 Listen Channel",
+    help: "Which channels to listen for messages on the nRF24",
+    type: "option_buttons",
+    options: {
+      'LOW': 'Min',
+      'MID': 'Mid',
+      'HIGH': 'High'
+    },
+    tab: "tab-radio"
+  }, {
+    tag:   "rf24_channels", 
+    friendly: "nRF24 Send Channels",
+    help: "Which channels to send messages on for the nRF24.  Using fewer channels speeds up sends.",
+    type: "option_buttons",
+    settings: {
+      multiple: true,
+    },
+    options: {
+      'LOW': 'Min',
+      'MID': 'Mid',
+      'HIGH': 'High'
+    },
+    tab: "tab-radio"
+  }, {
     tag:   "listen_repeats",
     friendly: "Listen repeats",
     help: "Increasing this increases the amount of time spent listening for " +
@@ -406,10 +431,19 @@ var loadSettings = function() {
 
     Object.keys(val).forEach(function(k) {
       var field = $('#settings input[name="' + k + '"]');
+      var selectVal = function(selectVal) {
+        field.filter('[value="' + selectVal + '"]').click();
+      };
 
       if (field.length > 0) {
-        if (field.attr('type') === 'radio') {
-          field.filter('[value="' + val[k] + '"]').click();
+        if (field.attr('type') === 'radio' || field.attr('type') === 'checkbox') {
+          if (Array.isArray(val[k])) {
+            val[k].forEach(function(x) {
+              selectVal(x);
+            });
+          } else {
+            selectVal(val[k]);
+          }
         } else {
           field.val(val[k]);
         }
@@ -719,13 +753,14 @@ var startSniffing = function() {
   $("#traffic-sniff").show();
 };
 
-var generateDropdownField = function(fieldName, options) {
+var generateDropdownField = function(fieldName, options, settings) {
   var s = '<div class="btn-group" id="' + fieldName + '" data-toggle="buttons">';
+  var inputType = settings.multiple ? 'checkbox' : 'radio';
 
   Object.keys(options).forEach(function(optionValue) {
     var optionLabel = options[optionValue];
-    s += '<label class="btn btn-secondary active">' +
-           '<input type="radio" id="' + fieldName + '" name="' + fieldName + '" autocomplete="off" value="' + optionValue + '" /> ' + optionLabel +
+    s += '<label class="btn btn-secondary">' +
+           '<input type="' + inputType + '" id="' + fieldName + '" name="' + fieldName + '" autocomplete="off" value="' + optionValue + '" /> ' + optionLabel +
          '</label>';
   });
 
@@ -938,7 +973,7 @@ $(function() {
           });
           elmt += '</select>';
         } else if (k.type == 'option_buttons') {
-          elmt += generateDropdownField(k.tag, k.options);
+          elmt += generateDropdownField(k.tag, k.options, k.settings || {});
         } else {
           elmt += '<input type="text" class="form-control" name="' + k.tag + '"/>';
         }
@@ -966,8 +1001,9 @@ $(function() {
     if ($('#tab-udp-gateways').hasClass('active')) {
       saveGatewayConfigs();
     } else {
-      var obj = $('#settings')
-        .serializeArray()
+      var obj = $('#settings').serializeArray();
+
+      obj = obj
         .reduce(function(a, x) {
           var val = a[x.name];
 
@@ -980,7 +1016,11 @@ $(function() {
           }
 
           return a;
-        }, {});
+        }, 
+        {
+          // Make sure the value is always an array, even if a single item is selected
+          rf24_channels: []
+        });
 
       // Make sure we're submitting a value for group_state_fields (will be empty
       // if no values were selected).