Parcourir la source

New feature: queued packet sends (#479)

* Add setting for number of packet repeats per loop
* Split raw send stuff out of MiLightClient
* Add new libs to handle packet queuing and sending
* Add test for raw commands
* Add RadioSwitchboard library
Chris Mullins il y a 6 ans
Parent
commit
52d5c73e1f

Fichier diff supprimé car celui-ci est trop grand
+ 2 - 2
dist/index.html.gz.h


+ 21 - 148
lib/MiLight/MiLightClient.cpp

@@ -6,81 +6,28 @@
 #include <TokenIterator.h>
 
 MiLightClient::MiLightClient(
-  std::shared_ptr<MiLightRadioFactory> radioFactory,
+  RadioSwitchboard& radioSwitchboard,
+  PacketSender& packetSender,
   GroupStateStore* stateStore,
-  Settings* settings
-)
-  : currentRadio(NULL),
-    currentRemote(NULL),
-    packetSentHandler(NULL),
-    updateBeginHandler(NULL),
-    updateEndHandler(NULL),
-    stateStore(stateStore),
-    settings(settings),
-    lastSend(0),
-    baseResendCount(MILIGHT_DEFAULT_RESEND_COUNT)
-{
-  for (size_t i = 0; i < MiLightRadioConfig::NUM_CONFIGS; i++) {
-    radios.push_back(radioFactory->create(MiLightRadioConfig::ALL_CONFIGS[i]));
-  }
-}
-
-void MiLightClient::begin() {
-  for (size_t i = 0; i < radios.size(); i++) {
-    radios[i]->begin();
-  }
-
-  switchRadio(static_cast<size_t>(0));
-
-  // Little gross to do this here as it's relying on global state.  A better alternative
-  // would be to statically construct remote config factories which take in a stateStore
-  // and settings pointer.  The objects could then be initialized by calling the factory
-  // in main.
-  for (size_t i = 0; i < MiLightRemoteConfig::NUM_REMOTES; i++) {
-    MiLightRemoteConfig::ALL_REMOTES[i]->packetFormatter->initialize(stateStore, settings);
-  }
-}
+  Settings& settings
+) : radioSwitchboard(radioSwitchboard)
+  , updateBeginHandler(NULL)
+  , updateEndHandler(NULL)
+  , stateStore(stateStore)
+  , settings(settings)
+  , packetSender(packetSender)
+  , repeatsOverride(0)
+{ }
 
 void MiLightClient::setHeld(bool held) {
   currentRemote->packetFormatter->setHeld(held);
 }
 
-size_t MiLightClient::getNumRadios() const {
-  return radios.size();
-}
-
-std::shared_ptr<MiLightRadio> MiLightClient::switchRadio(size_t radioIx) {
-  if (radioIx >= getNumRadios()) {
-    return NULL;
-  }
-
-  if (this->currentRadio != radios[radioIx]) {
-    this->currentRadio = radios[radioIx];
-    this->currentRadio->configure();
-  }
-
-  return this->currentRadio;
-}
-
-std::shared_ptr<MiLightRadio> MiLightClient::switchRadio(const MiLightRemoteConfig* remoteConfig) {
-  std::shared_ptr<MiLightRadio> radio = NULL;
-
-  for (size_t i = 0; i < radios.size(); i++) {
-    if (&this->radios[i]->config() == &remoteConfig->radioConfig) {
-      radio = switchRadio(i);
-      break;
-    }
-  }
-
-  return radio;
-}
-
-void MiLightClient::prepare(const MiLightRemoteConfig* config,
+void MiLightClient::prepare(
+  const MiLightRemoteConfig* config,
   const uint16_t deviceId,
   const uint8_t groupId
 ) {
-  switchRadio(config);
-
   this->currentRemote = config;
 
   if (deviceId >= 0 && groupId >= 0) {
@@ -88,70 +35,14 @@ void MiLightClient::prepare(const MiLightRemoteConfig* config,
   }
 }
 
-void MiLightClient::prepare(const MiLightRemoteType type,
+void MiLightClient::prepare(
+  const MiLightRemoteType type,
   const uint16_t deviceId,
   const uint8_t groupId
 ) {
   prepare(MiLightRemoteConfig::fromType(type), deviceId, groupId);
 }
 
-void MiLightClient::setResendCount(const unsigned int resendCount) {
-  this->baseResendCount = resendCount;
-  this->currentResendCount = resendCount;
-  this->throttleMultiplier = ceil((settings->packetRepeatThrottleSensitivity / 1000.0) * this->baseResendCount);
-}
-
-bool MiLightClient::available() {
-  if (currentRadio == NULL) {
-    return false;
-  }
-
-  return currentRadio->available();
-}
-
-size_t MiLightClient::read(uint8_t packet[]) {
-  if (currentRadio == NULL) {
-    return 0;
-  }
-
-  size_t length;
-  currentRadio->read(packet, length);
-
-  return length;
-}
-
-void MiLightClient::write(uint8_t packet[]) {
-  if (currentRadio == NULL) {
-    return;
-  }
-
-#ifdef DEBUG_PRINTF
-  Serial.printf_P(PSTR("Sending packet (%d repeats): \n"), this->currentResendCount);
-  for (int i = 0; i < currentRemote->packetFormatter->getPacketLength(); i++) {
-    Serial.printf_P(PSTR("%02X "), packet[i]);
-  }
-  Serial.println();
-  int iStart = millis();
-#endif
-
-  // send the packet out (multiple times for "reliability")
-  for (int i = 0; i < this->currentResendCount; i++) {
-    currentRadio->write(packet, currentRemote->packetFormatter->getPacketLength());
-  }
-
-  // if we have a packetSendHandler defined (see MiLightClient::onPacketSent), call it now that
-  // the packet has been dispatched
-  if (this->packetSentHandler) {
-    this->packetSentHandler(packet, *currentRemote);
-  }
-
-#ifdef DEBUG_PRINTF
-  int iElapsed = millis() - iStart;
-  Serial.print("Elapsed: ");
-  Serial.println(iElapsed);
-#endif
-}
-
 void MiLightClient::updateColorRaw(const uint8_t color) {
 #ifdef DEBUG_CLIENT_COMMANDS
   Serial.printf_P(PSTR("MiLightClient::updateColorRaw: Change color to %d\n"), color);
@@ -514,42 +405,24 @@ uint8_t MiLightClient::parseStatus(JsonObject object) {
   }
 }
 
-void MiLightClient::updateResendCount() {
-  unsigned long now = millis();
-  long millisSinceLastSend = now - lastSend;
-  long x = (millisSinceLastSend - settings->packetRepeatThrottleThreshold);
-  long delta = x * throttleMultiplier;
+void MiLightClient::setRepeatsOverride(size_t repeats) {
+  this->repeatsOverride = repeats;
+}
 
-  this->currentResendCount = constrain(
-    static_cast<size_t>(this->currentResendCount + delta),
-    settings->packetRepeatMinimum,
-    this->baseResendCount
-  );
-  this->lastSend = now;
+void MiLightClient::clearRepeatsOverride() {
+  this->repeatsOverride = PacketSender::DEFAULT_PACKET_SENDS_VALUE;
 }
 
 void MiLightClient::flushPacket() {
   PacketStream& stream = currentRemote->packetFormatter->buildPackets();
-  updateResendCount();
 
   while (stream.hasNext()) {
-    write(stream.next());
-
-    if (stream.hasNext()) {
-      delay(10);
-    }
+    packetSender.enqueue(stream.next(), currentRemote, repeatsOverride);
   }
 
   currentRemote->packetFormatter->reset();
 }
 
-/*
-  Register a callback for when packets are sent
-*/
-void MiLightClient::onPacketSent(PacketSentHandler handler) {
-  this->packetSentHandler = handler;
-}
-
 void MiLightClient::onUpdateBegin(EventHandler handler) {
   this->updateBeginHandler = handler;
 }

+ 16 - 34
lib/MiLight/MiLightClient.h

@@ -5,6 +5,7 @@
 #include <MiLightRemoteConfig.h>
 #include <Settings.h>
 #include <GroupStateStore.h>
+#include <PacketSender.h>
 
 #ifndef _MILIGHTCLIENT_H
 #define _MILIGHTCLIENT_H
@@ -12,25 +13,22 @@
 //#define DEBUG_PRINTF
 //#define DEBUG_CLIENT_COMMANDS     // enable to show each individual change command (like hue, brightness, etc)
 
-#define MILIGHT_DEFAULT_RESEND_COUNT 10
-
 // Used to determine RGB colros that are approximately white
 #define RGB_WHITE_THRESHOLD 10
 
 class MiLightClient {
 public:
   MiLightClient(
-    std::shared_ptr<MiLightRadioFactory> radioFactory,
+    RadioSwitchboard& radioSwitchboard,
+    PacketSender& packetSender,
     GroupStateStore* stateStore,
-    Settings* settings
+    Settings& settings
   );
 
   ~MiLightClient() { }
 
-  typedef std::function<void(uint8_t* packet, const MiLightRemoteConfig& config)> PacketSentHandler;
   typedef std::function<void(void)> EventHandler;
 
-  void begin();
   void prepare(const MiLightRemoteConfig* remoteConfig, const uint16_t deviceId = -1, const uint8_t groupId = -1);
   void prepare(const MiLightRemoteType type, const uint16_t deviceId = -1, const uint8_t groupId = -1);
 
@@ -74,7 +72,6 @@ public:
   void handleCommand(const String& command);
   void handleEffect(const String& effect);
 
-  void onPacketSent(PacketSentHandler handler);
   void onUpdateBegin(EventHandler handler);
   void onUpdateEnd(EventHandler handler);
 
@@ -83,42 +80,27 @@ public:
   std::shared_ptr<MiLightRadio> switchRadio(const MiLightRemoteConfig* remoteConfig);
   MiLightRemoteConfig& currentRemoteConfig() const;
 
-protected:
+  // Call to override the number of packet repeats that are sent.  Clear with clearRepeatsOverride
+  void setRepeatsOverride(size_t repeatsOverride);
+
+  // Clear the repeats override so that the default is used
+  void clearRepeatsOverride();
 
+protected:
+  RadioSwitchboard& radioSwitchboard;
   std::vector<std::shared_ptr<MiLightRadio>> radios;
   std::shared_ptr<MiLightRadio> currentRadio;
   const MiLightRemoteConfig* currentRemote;
 
-  PacketSentHandler packetSentHandler;
   EventHandler updateBeginHandler;
   EventHandler updateEndHandler;
 
   GroupStateStore* stateStore;
-  const Settings* settings;
-
-  // Used to track auto repeat limiting
-  unsigned long lastSend;
-  uint8_t currentResendCount;
-  unsigned int baseResendCount;
-
-  // This will be pre-computed, but is simply:
-  //
-  //    (sensitivity / 1000.0) * R
-  //
-  // Where R is the base number of repeats.
-  size_t throttleMultiplier;
-
-  /*
-   * Calculates the number of resend packets based on when the last packet
-   * was sent using this function:
-   *
-   *    lastRepeatsValue + (millisSinceLastSend - THRESHOLD) * throttleMultiplier
-   *
-   * When the last send was more recent than THRESHOLD, the number of repeats
-   * will be decreased to a minimum of zero.  When less recent, it will be
-   * increased up to a maximum of the default resend count.
-   */
-  void updateResendCount();
+  Settings& settings;
+  PacketSender& packetSender;
+
+  // If set, override the number of packet repeats used.
+  size_t repeatsOverride;
 
   uint8_t parseStatus(JsonObject object);
 

+ 33 - 0
lib/MiLight/PacketQueue.cpp

@@ -0,0 +1,33 @@
+#include <PacketQueue.h>
+
+void PacketQueue::push(const uint8_t* packet, const MiLightRemoteConfig* remoteConfig, const size_t repeatsOverride) {
+  std::shared_ptr<QueuedPacket> qp = checkoutPacket();
+  memcpy(qp->packet, packet, remoteConfig->packetFormatter->getPacketLength());
+  qp->remoteConfig = remoteConfig;
+  qp->repeatsOverride = repeatsOverride;
+}
+
+bool PacketQueue::isEmpty() {
+  return queue.size() == 0;
+}
+
+std::shared_ptr<QueuedPacket> PacketQueue::pop() {
+  return queue.shift();
+}
+
+std::shared_ptr<QueuedPacket> PacketQueue::checkoutPacket() {
+  if (queue.size() == MILIGHT_MAX_QUEUED_PACKETS) {
+    return queue.getLast();
+  } else {
+    std::shared_ptr<QueuedPacket> packet = std::make_shared<QueuedPacket>();
+    queue.add(packet);
+    return packet;
+  }
+}
+
+void PacketQueue::checkinPacket(std::shared_ptr<QueuedPacket> packet) {
+}
+
+size_t PacketQueue::size() {
+  return queue.size();
+}

+ 31 - 0
lib/MiLight/PacketQueue.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <memory>
+
+#include <CircularBuffer.h>
+#include <MiLightRadioConfig.h>
+#include <MiLightRemoteConfig.h>
+
+#ifndef MILIGHT_MAX_QUEUED_PACKETS
+#define MILIGHT_MAX_QUEUED_PACKETS 20
+#endif
+
+struct QueuedPacket {
+  uint8_t packet[MILIGHT_MAX_PACKET_LENGTH];
+  const MiLightRemoteConfig* remoteConfig;
+  size_t repeatsOverride;
+};
+
+class PacketQueue {
+public:
+  void push(const uint8_t* packet, const MiLightRemoteConfig* remoteConfig, const size_t repeatsOverride);
+  std::shared_ptr<QueuedPacket> pop();
+  bool isEmpty();
+  size_t size();
+
+private:
+  std::shared_ptr<QueuedPacket> checkoutPacket();
+  void checkinPacket(std::shared_ptr<QueuedPacket> packet);
+
+  LinkedList<std::shared_ptr<QueuedPacket>> queue;
+};

+ 118 - 0
lib/MiLight/PacketSender.cpp

@@ -0,0 +1,118 @@
+#include <PacketSender.h>
+#include <MiLightRadioConfig.h>
+
+PacketSender::PacketSender(
+  RadioSwitchboard& radioSwitchboard,
+  Settings& settings,
+  PacketSentHandler packetSentHandler
+) : radioSwitchboard(radioSwitchboard)
+  , settings(settings)
+  , stateStore(stateStore)
+  , currentPacket(nullptr)
+  , packetRepeatsRemaining(0)
+  , packetSentHandler(packetSentHandler)
+  , lastSend(0)
+  , currentResendCount(settings.packetRepeats)
+  , throttleMultiplier(
+      std::ceil(
+        (settings.packetRepeatThrottleSensitivity / 1000.0) * settings.packetRepeats
+      )
+    )
+{ }
+
+void PacketSender::enqueue(uint8_t* packet, const MiLightRemoteConfig* remoteConfig, const size_t repeatsOverride) {
+#ifdef DEBUG_PRINTF
+  Serial.println("Enqueuing packet");
+#endif
+  size_t repeats = repeatsOverride == DEFAULT_PACKET_SENDS_VALUE
+    ? this->currentResendCount
+    : repeatsOverride;
+
+  queue.push(packet, remoteConfig, repeats);
+}
+
+void PacketSender::loop() {
+  // Switch to the next packet if we're done with the current one
+  if (packetRepeatsRemaining == 0 && !queue.isEmpty()) {
+    nextPacket();
+  }
+
+  // If there's a packet we're handling, deal with it
+  if (currentPacket != nullptr && packetRepeatsRemaining > 0) {
+    handleCurrentPacket();
+  }
+}
+
+bool PacketSender::isSending() {
+  return packetRepeatsRemaining > 0 || !queue.isEmpty();
+}
+
+void PacketSender::nextPacket() {
+#ifdef DEBUG_PRINTF
+  Serial.printf("Switching to next packet, %d packets in queue\n", queue.size());
+#endif
+  currentPacket = queue.pop();
+
+  if (currentPacket->repeatsOverride > 0) {
+    packetRepeatsRemaining = currentPacket->repeatsOverride;
+  } else {
+    packetRepeatsRemaining = settings.packetRepeats;
+  }
+
+  // Adjust resend count according to throttling rules
+  updateResendCount();
+}
+
+void PacketSender::handleCurrentPacket() {
+  // Always switch radio.  could've been listening in another context
+  radioSwitchboard.switchRadio(currentPacket->remoteConfig);
+
+  size_t numToSend = std::min(packetRepeatsRemaining, settings.packetRepeatsPerLoop);
+  sendRepeats(numToSend);
+  packetRepeatsRemaining -= numToSend;
+
+  // If we're done sending this packet, fire the sent packet callback
+  if (packetRepeatsRemaining == 0 && packetSentHandler != nullptr) {
+    packetSentHandler(currentPacket->packet, *currentPacket->remoteConfig);
+  }
+}
+
+void PacketSender::sendRepeats(size_t num) {
+  size_t len = currentPacket->remoteConfig->packetFormatter->getPacketLength();
+
+#ifdef DEBUG_PRINTF
+  Serial.printf_P(PSTR("Sending packet (%d repeats): \n"), num);
+  for (size_t i = 0; i < len; i++) {
+    Serial.printf_P(PSTR("%02X "), currentPacket->packet[i]);
+  }
+  Serial.println();
+  int iStart = millis();
+#endif
+
+  for (size_t i = 0; i < num; ++i) {
+    radioSwitchboard.write(currentPacket->packet, len);
+  }
+
+#ifdef DEBUG_PRINTF
+  int iElapsed = millis() - iStart;
+  Serial.print("Elapsed: ");
+  Serial.println(iElapsed);
+#endif
+}
+
+void PacketSender::updateResendCount() {
+  unsigned long now = millis();
+  long millisSinceLastSend = now - lastSend;
+  long x = (millisSinceLastSend - settings.packetRepeatThrottleThreshold);
+  long delta = x * throttleMultiplier;
+  int signedResends = static_cast<int>(this->currentResendCount) + delta;
+
+  if (signedResends < static_cast<int>(settings.packetRepeatMinimum)) {
+    signedResends = settings.packetRepeatMinimum;
+  } else if (signedResends > settings.packetRepeats) {
+    signedResends = settings.packetRepeats;
+  }
+
+  this->currentResendCount = signedResends;
+  this->lastSend = now;
+}

+ 70 - 0
lib/MiLight/PacketSender.h

@@ -0,0 +1,70 @@
+#pragma once
+
+#include <MiLightRadioFactory.h>
+#include <MiLightRemoteConfig.h>
+#include <PacketQueue.h>
+#include <RadioSwitchboard.h>
+
+class PacketSender {
+public:
+  typedef std::function<void(uint8_t* packet, const MiLightRemoteConfig& config)> PacketSentHandler;
+  static const size_t DEFAULT_PACKET_SENDS_VALUE = 0;
+
+  PacketSender(
+    RadioSwitchboard& radioSwitchboard,
+    Settings& settings,
+    PacketSentHandler packetSentHandler
+  );
+
+  void enqueue(uint8_t* packet, const MiLightRemoteConfig* remoteConfig, const size_t repeatsOverride = 0);
+  void loop();
+
+  // Return true if there are queued packets
+  bool isSending();
+
+private:
+  RadioSwitchboard& radioSwitchboard;
+  Settings& settings;
+  GroupStateStore* stateStore;
+  PacketQueue queue;
+
+  // The current packet we're sending and the number of repeats left
+  std::shared_ptr<QueuedPacket> currentPacket;
+  size_t packetRepeatsRemaining;
+
+  // Handler called after packets are sent.  Will not be called multiple times
+  // per repeat.
+  PacketSentHandler packetSentHandler;
+
+  // Send a batch of repeats for the current packet
+  void handleCurrentPacket();
+
+  // Switch to the next packet in the queue
+  void nextPacket();
+
+  // Send repeats of the current packet N times
+  void sendRepeats(size_t num);
+
+  // Used to track auto repeat limiting
+  unsigned long lastSend;
+  uint8_t currentResendCount;
+
+  // This will be pre-computed, but is simply:
+  //
+  //    (sensitivity / 1000.0) * R
+  //
+  // Where R is the base number of repeats.
+  size_t throttleMultiplier;
+
+  /*
+   * Calculates the number of resend packets based on when the last packet
+   * was sent using this function:
+   *
+   *    lastRepeatsValue + (millisSinceLastSend - THRESHOLD) * throttleMultiplier
+   *
+   * When the last send was more recent than THRESHOLD, the number of repeats
+   * will be decreased to a minimum of zero.  When less recent, it will be
+   * increased up to a maximum of the default resend count.
+   */
+  void updateResendCount();
+};

+ 74 - 0
lib/MiLight/RadioSwitchboard.cpp

@@ -0,0 +1,74 @@
+#include <RadioSwitchboard.h>
+
+RadioSwitchboard::RadioSwitchboard(
+  std::shared_ptr<MiLightRadioFactory> radioFactory,
+  GroupStateStore* stateStore,
+  Settings& settings
+) {
+  for (size_t i = 0; i < MiLightRadioConfig::NUM_CONFIGS; i++) {
+    std::shared_ptr<MiLightRadio> radio = radioFactory->create(MiLightRadioConfig::ALL_CONFIGS[i]);
+    radio->begin();
+    radios.push_back(radio);
+  }
+
+  for (size_t i = 0; i < MiLightRemoteConfig::NUM_REMOTES; i++) {
+    MiLightRemoteConfig::ALL_REMOTES[i]->packetFormatter->initialize(stateStore, &settings);
+  }
+}
+
+size_t RadioSwitchboard::getNumRadios() const {
+  return radios.size();
+}
+
+std::shared_ptr<MiLightRadio> RadioSwitchboard::switchRadio(size_t radioIx) {
+  if (radioIx >= getNumRadios()) {
+    return NULL;
+  }
+
+  if (this->currentRadio != radios[radioIx]) {
+    this->currentRadio = radios[radioIx];
+    this->currentRadio->configure();
+  }
+
+  return this->currentRadio;
+}
+
+std::shared_ptr<MiLightRadio> RadioSwitchboard::switchRadio(const MiLightRemoteConfig* remote) {
+  std::shared_ptr<MiLightRadio> radio = NULL;
+
+  for (size_t i = 0; i < radios.size(); i++) {
+    if (&this->radios[i]->config() == &remote->radioConfig) {
+      radio = switchRadio(i);
+      break;
+    }
+  }
+
+  return radio;
+}
+
+void RadioSwitchboard::write(uint8_t* packet, size_t len) {
+  if (this->currentRadio == nullptr) {
+    return;
+  }
+
+  this->currentRadio->write(packet, len);
+}
+
+size_t RadioSwitchboard::read(uint8_t* packet) {
+  if (currentRadio == nullptr) {
+    return 0;
+  }
+
+  size_t length;
+  currentRadio->read(packet, length);
+
+  return length;
+}
+
+bool RadioSwitchboard::available() {
+  if (currentRadio == nullptr) {
+    return false;
+  }
+
+  return currentRadio->available();
+}

+ 27 - 0
lib/MiLight/RadioSwitchboard.h

@@ -0,0 +1,27 @@
+#pragma once
+
+#include <MiLightRadio.h>
+#include <MiLightRemoteConfig.h>
+#include <MiLightRadioConfig.h>
+#include <MiLightRadioFactory.h>
+
+class RadioSwitchboard {
+public:
+  RadioSwitchboard(
+    std::shared_ptr<MiLightRadioFactory> radioFactory,
+    GroupStateStore* stateStore,
+    Settings& settings
+  );
+
+  std::shared_ptr<MiLightRadio> switchRadio(const MiLightRemoteConfig* remote);
+  std::shared_ptr<MiLightRadio> switchRadio(size_t index);
+  size_t getNumRadios() const;
+
+  bool available();
+  void write(uint8_t* packet, size_t length);
+  size_t read(uint8_t* packet);
+
+private:
+  std::vector<std::shared_ptr<MiLightRadio>> radios;
+  std::shared_ptr<MiLightRadio> currentRadio;
+};

+ 2 - 0
lib/Settings/Settings.cpp

@@ -97,6 +97,7 @@ void Settings::patch(JsonObject parsedSettings) {
   this->setIfPresent(parsedSettings, "wifi_static_ip", wifiStaticIP);
   this->setIfPresent(parsedSettings, "wifi_static_ip_gateway", wifiStaticIPGateway);
   this->setIfPresent(parsedSettings, "wifi_static_ip_netmask", wifiStaticIPNetmask);
+  this->setIfPresent(parsedSettings, "packet_repeats_per_loop", packetRepeatsPerLoop);
 
   if (parsedSettings.containsKey("rf24_channels")) {
     JsonArray arr = parsedSettings["rf24_channels"];
@@ -226,6 +227,7 @@ void Settings::serialize(Print& stream, const bool prettyPrint) {
   root["wifi_static_ip"] = this->wifiStaticIP;
   root["wifi_static_ip_gateway"] = this->wifiStaticIPGateway;
   root["wifi_static_ip_netmask"] = this->wifiStaticIPNetmask;
+  root["packet_repeats_per_loop"] = this->packetRepeatsPerLoop;
 
   JsonArray channelArr = root.createNestedArray("rf24_channels");
   JsonHelpers::vectorToJsonArr<RF24Channel, String>(channelArr, rf24Channels, RF24ChannelHelpers::nameFromValue);

+ 2 - 0
lib/Settings/Settings.h

@@ -107,6 +107,7 @@ public:
     rf24Channels(RF24ChannelHelpers::allValues()),
     groupStateFields(DEFAULT_GROUP_STATE_FIELDS),
     rf24ListenChannel(RF24Channel::RF24_LOW),
+    packetRepeatsPerLoop(10),
     _autoRestartPeriod(0)
   { }
 
@@ -174,6 +175,7 @@ public:
   String wifiStaticIP;
   String wifiStaticIPNetmask;
   String wifiStaticIPGateway;
+  size_t packetRepeatsPerLoop;
 
 protected:
   size_t _autoRestartPeriod;

+ 18 - 9
lib/WebServer/MiLightHttpServer.cpp

@@ -270,7 +270,7 @@ void MiLightHttpServer::handleListenGateway(RequestContext& request) {
   }
 
   if (tmpRemoteConfig != NULL) {
-    radio = milightClient->switchRadio(tmpRemoteConfig);
+    radio = radios->switchRadio(tmpRemoteConfig);
   }
 
   while (remoteConfig == NULL) {
@@ -279,13 +279,13 @@ void MiLightHttpServer::handleListenGateway(RequestContext& request) {
     }
 
     if (listenAll) {
-      radio = milightClient->switchRadio(configIx++ % milightClient->getNumRadios());
+      radio = radios->switchRadio(configIx++ % radios->getNumRadios());
     } else {
       radio->configure();
     }
 
-    if (milightClient->available()) {
-      size_t packetLen = milightClient->read(packet);
+    if (radios->available()) {
+      size_t packetLen = radios->read(packet);
       remoteConfig = MiLightRemoteConfig::fromReceivedPacket(
         radio->config(),
         packet,
@@ -362,7 +362,7 @@ void MiLightHttpServer::handleDeleteGroup(RequestContext& request) {
 void MiLightHttpServer::handleUpdateGroup(RequestContext& request) {
   JsonObject reqObj = request.getJsonBody().as<JsonObject>();
 
-  milightClient->setResendCount(
+  milightClient->setRepeatsOverride(
     settings.httpRepeatFactor * settings.packetRepeats
   );
 
@@ -411,7 +411,15 @@ void MiLightHttpServer::handleUpdateGroup(RequestContext& request) {
     }
   }
 
+  milightClient->clearRepeatsOverride();
+
   if (groupCount == 1) {
+    // Wait for packet queue to flush out.  State will not have been updated before that.
+    // Bit hacky to call loop outside of main loop, but should be fine.
+    while (packetSender->isSending()) {
+      packetSender->loop();
+    }
+
     sendGroupState(foundBulbId, stateStore->get(foundBulbId), request.response);
   } else {
     request.response.json["success"] = true;
@@ -438,15 +446,16 @@ void MiLightHttpServer::handleSendRaw(RequestContext& request) {
   const String& hexPacket = requestBody["packet"];
   hexStrToBytes<uint8_t>(hexPacket.c_str(), hexPacket.length(), packet, MILIGHT_MAX_PACKET_LENGTH);
 
-  size_t numRepeats = MILIGHT_DEFAULT_RESEND_COUNT;
+  size_t numRepeats = settings.packetRepeats;
   if (requestBody.containsKey("num_repeats")) {
     numRepeats = requestBody["num_repeats"];
   }
 
-  milightClient->prepare(config, 0, 0);
+  packetSender->enqueue(packet, config, numRepeats);
 
-  for (size_t i = 0; i < numRepeats; i++) {
-    milightClient->write(packet);
+  // To make this response synchronous, wait for packet to be flushed
+  while (packetSender->isSending()) {
+    packetSender->loop();
   }
 
   request.response.json["success"] = true;

+ 13 - 1
lib/WebServer/MiLightHttpServer.h

@@ -3,6 +3,8 @@
 #include <Settings.h>
 #include <WebSocketsServer.h>
 #include <GroupStateStore.h>
+#include <RadioSwitchboard.h>
+#include <PacketSender.h>
 
 #ifndef _MILIGHT_HTTP_SERVER
 #define _MILIGHT_HTTP_SERVER
@@ -20,7 +22,13 @@ const char APPLICATION_JSON[] = "application/json";
 
 class MiLightHttpServer {
 public:
-  MiLightHttpServer(Settings& settings, MiLightClient*& milightClient, GroupStateStore*& stateStore)
+  MiLightHttpServer(
+    Settings& settings,
+    MiLightClient*& milightClient,
+    GroupStateStore*& stateStore,
+    PacketSender*& packetSender,
+    RadioSwitchboard*& radios
+  )
     : authProvider(settings)
     , server(80, authProvider)
     , wsServer(WebSocketsServer(81))
@@ -28,6 +36,8 @@ public:
     , milightClient(milightClient)
     , settings(settings)
     , stateStore(stateStore)
+    , packetSender(packetSender)
+    , radios(radios)
   { }
 
   void begin();
@@ -76,6 +86,8 @@ protected:
   SettingsSavedHandler settingsSavedHandler;
   GroupDeletedHandler groupDeletedHandler;
   ESP8266WebServer::THandlerFunction _handleRootPage;
+  PacketSender*& packetSender;
+  RadioSwitchboard*& radios;
 
 };
 

+ 25 - 10
src/main.cpp

@@ -22,6 +22,8 @@
 #include <MiLightClient.h>
 #include <BulbStateUpdater.h>
 #include <LEDStatus.h>
+#include <RadioSwitchboard.h>
+#include <PacketSender.h>
 
 #include <vector>
 #include <memory>
@@ -37,6 +39,8 @@ static LEDStatus *ledStatus;
 Settings settings;
 
 MiLightClient* milightClient = NULL;
+RadioSwitchboard* radios = nullptr;
+PacketSender* packetSender = nullptr;
 std::shared_ptr<MiLightRadioFactory> radioFactory;
 MiLightHttpServer *httpServer = NULL;
 MqttClient* mqttClient = NULL;
@@ -133,16 +137,19 @@ void onPacketSentHandler(uint8_t* packet, const MiLightRemoteConfig& config) {
  * called.
  */
 void handleListen() {
-  if (! settings.listenRepeats) {
+  // Do not handle listens while there are packets enqueued to be sent
+  // Doing so causes the radio module to need to be reinitialized inbetween
+  // repeats, which slows things down.
+  if (! settings.listenRepeats || packetSender->isSending()) {
     return;
   }
 
-  std::shared_ptr<MiLightRadio> radio = milightClient->switchRadio(currentRadioType++ % milightClient->getNumRadios());
+  std::shared_ptr<MiLightRadio> radio = radios->switchRadio(currentRadioType++ % radios->getNumRadios());
 
   for (size_t i = 0; i < settings.listenRepeats; i++) {
-    if (milightClient->available()) {
+    if (radios->available()) {
       uint8_t readPacket[MILIGHT_MAX_PACKET_LENGTH];
-      size_t packetLen = milightClient->read(readPacket);
+      size_t packetLen = radios->read(readPacket);
 
       const MiLightRemoteConfig* remoteConfig = MiLightRemoteConfig::fromReceivedPacket(
         radio->config(),
@@ -201,6 +208,12 @@ void applySettings() {
   if (stateStore) {
     delete stateStore;
   }
+  if (packetSender) {
+    delete packetSender;
+  }
+  if (radios) {
+    delete radios;
+  }
 
   radioFactory = MiLightRadioFactory::fromSettings(settings);
 
@@ -210,16 +223,17 @@ void applySettings() {
 
   stateStore = new GroupStateStore(MILIGHT_MAX_STATE_ITEMS, settings.stateFlushInterval);
 
+  radios = new RadioSwitchboard(radioFactory, stateStore, settings);
+  packetSender = new PacketSender(*radios, settings, onPacketSentHandler);
+
   milightClient = new MiLightClient(
-    radioFactory,
+    *radios,
+    *packetSender,
     stateStore,
-    &settings
+    settings
   );
-  milightClient->begin();
-  milightClient->onPacketSent(onPacketSentHandler);
   milightClient->onUpdateBegin(onUpdateBegin);
   milightClient->onUpdateEnd(onUpdateEnd);
-  milightClient->setResendCount(settings.packetRepeats);
 
   if (settings.mqttServer().length() > 0) {
     mqttClient = new MqttClient(settings, milightClient);
@@ -375,7 +389,7 @@ void setup() {
   SSDP.setDeviceType("upnp:rootdevice");
   SSDP.begin();
 
-  httpServer = new MiLightHttpServer(settings, milightClient, stateStore);
+  httpServer = new MiLightHttpServer(settings, milightClient, stateStore, packetSender, radios);
   httpServer->onSettingsSaved(applySettings);
   httpServer->onGroupDeleted(onGroupDeleted);
   httpServer->on("/description.xml", HTTP_GET, []() { SSDP.schema(httpServer->client()); });
@@ -403,6 +417,7 @@ void loop() {
   handleListen();
 
   stateStore->limitedFlush();
+  packetSender->loop();
 
   // update LED with status
   ledStatus->handle();

+ 5 - 1
test/remote/spec/mqtt_spec.rb

@@ -1,6 +1,6 @@
 require 'api_client'
 
-RSpec.describe 'State' do
+RSpec.describe 'MQTT' do
   before(:all) do
     @client = ApiClient.new(ENV.fetch('ESPMH_HOSTNAME'), ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE'))
     @client.upload_json('/settings', 'settings.json')
@@ -104,6 +104,10 @@ RSpec.describe 'State' do
       @client.patch_state({level: 50, status: 'off'}, @id_params)
 
       @mqtt_client.patch_state(@id_params, status: 'on', level: 70)
+
+      # wait for packet to be sent...
+      sleep(1)
+
       state = @client.get_state(@id_params)
 
       expect(state.keys).to      include(*%w(level status))

+ 25 - 0
test/remote/spec/rest_spec.rb

@@ -85,4 +85,29 @@ RSpec.describe 'REST Server' do
       expect(result).to include('rgb_cct')
     end
   end
+
+  context 'sending raw packets' do
+    it 'should support sending a raw packet' do
+      id = {
+        id: 0x2222,
+        type: 'rgb_cct',
+        group_id: 1
+      }
+      @client.delete_state(id)
+
+      # Hard-coded packet which should turn the bulb on
+      result = @client.post(
+        '/raw_commands/rgb_cct',
+        packet: '00 DB BF 01 66 D1 BB 66 F7',
+        num_repeats: 1
+      )
+      expect(result['success']).to be_truthy
+
+      sleep(1)
+
+      state = @client.get_state(id)
+      puts state.inspect
+      expect(state['status']).to eq('ON')
+    end
+  end
 end

+ 2 - 1
test/remote/spec/settings_spec.rb

@@ -26,7 +26,8 @@ RSpec.describe 'Settings' do
   context 'keys' do
     it 'should persist known settings keys' do
       {
-        'simple_mqtt_client_status' => [true, false]
+        'simple_mqtt_client_status' => [true, false],
+        'packet_repeats_per_loop' => [10]
       }.each do |key, values|
         values.each do |v|
           @client.patch_settings({key => v})

+ 6 - 0
web/src/js/script.js

@@ -109,6 +109,12 @@ var UI_FIELDS = [ {
     type: "string",
     tab: "tab-radio"
   }, {
+    tag: "packet_repeats_per_loop",
+    friendly: "Packet repeats per loop",
+    help: "Number of repeats to send in a single go.  Higher values mean more throughput, but less multitasking.",
+    type: "string",
+    tab: "tab-radio"
+  }, {
     tag: "http_repeat_factor",
     friendly: "HTTP repeat factor",
     help: "Multiplicative factor on packet_repeats for requests initiated by the HTTP API. UDP API typically receives " +