#ifndef UNIT_TEST #define DEBUG_PRINTF #define DEBUG_SERIAL #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ESP8266 #include #include #elif ESP32 #include #include #endif #include #include WiFiManager wifiManager; // because of callbacks, these need to be in the higher scope :( WiFiManagerParameter* wifiStaticIP = NULL; WiFiManagerParameter* wifiStaticIPNetmask = NULL; WiFiManagerParameter* wifiStaticIPGateway = NULL; static LEDStatus *ledStatus; Settings settings; MiLightClient* milightClient = NULL; RadioSwitchboard* radios = nullptr; PacketSender* packetSender = nullptr; std::shared_ptr radioFactory; MiLightHttpServer *httpServer = NULL; MqttClient* mqttClient = NULL; MiLightDiscoveryServer* discoveryServer = NULL; uint8_t currentRadioType = 0; // For tracking and managing group state GroupStateStore* stateStore = NULL; BulbStateUpdater* bulbStateUpdater = NULL; TransitionController transitions; int numUdpServers = 0; std::vector> udpServers; WiFiUDP udpSeder; /** * Set up UDP servers (both v5 and v6). Clean up old ones if necessary. */ void initMilightUdpServers() { udpServers.clear(); for (size_t i = 0; i < settings.gatewayConfigs.size(); ++i) { const GatewayConfig& config = *settings.gatewayConfigs[i]; std::shared_ptr server = MiLightUdpServer::fromVersion( config.protocolVersion, milightClient, config.port, config.deviceId ); if (server == NULL) { Serial.print(F("Error creating UDP server with protocol version: ")); Serial.println(config.protocolVersion); } else { udpServers.push_back(std::move(server)); udpServers[i]->begin(); } } } /** * Milight RF packet handler. * * Called both when a packet is sent locally, and when an intercepted packet * is read. */ void onPacketSentHandler(uint8_t* packet, const MiLightRemoteConfig& config) { StaticJsonDocument<200> buffer; JsonObject result = buffer.to(); BulbId bulbId = config.packetFormatter->parsePacket(packet, result); // set LED mode for a packet movement ledStatus->oneshot(settings.ledModePacket, settings.ledModePacketCount); if (&bulbId == &DEFAULT_BULB_ID) { Serial.println(F("Skipping packet handler because packet was not decoded")); return; } const MiLightRemoteConfig& remoteConfig = *MiLightRemoteConfig::fromType(bulbId.deviceType); // update state to reflect changes from this packet GroupState* groupState = stateStore->get(bulbId); // pass in previous scratch state as well const GroupState stateUpdates(groupState, result); if (groupState != NULL) { groupState->patch(stateUpdates); // Copy state before setting it to avoid group 0 re-initialization clobbering it stateStore->set(bulbId, stateUpdates); } if (mqttClient) { // Sends the state delta derived from the raw packet char output[200]; serializeJson(result, output); mqttClient->sendUpdate(remoteConfig, bulbId.deviceId, bulbId.groupId, output); // Sends the entire state if (groupState != NULL) { bulbStateUpdater->enqueueUpdate(bulbId, *groupState); } } httpServer->handlePacketSent(packet, remoteConfig); } /** * Listen for packets on one radio config. Cycles through all configs as its * called. */ void handleListen() { // 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 radio = radios->switchRadio(currentRadioType++ % radios->getNumRadios()); for (size_t i = 0; i < settings.listenRepeats; i++) { if (radios->available()) { uint8_t readPacket[MILIGHT_MAX_PACKET_LENGTH]; size_t packetLen = radios->read(readPacket); const MiLightRemoteConfig* remoteConfig = MiLightRemoteConfig::fromReceivedPacket( radio->config(), readPacket, packetLen ); if (remoteConfig == NULL) { // This can happen under normal circumstances, so not an error condition #ifdef DEBUG_PRINTF Serial.println(F("WARNING: Couldn't find remote for received packet")); #endif return; } // update state to reflect this packet onPacketSentHandler(readPacket, *remoteConfig); } } } /** * Called when MqttClient#update is first being processed. Stop sending updates * and aggregate state changes until the update is finished. */ void onUpdateBegin() { if (bulbStateUpdater) { bulbStateUpdater->disable(); } } /** * Called when MqttClient#update is finished processing. Re-enable state * updates, which will flush accumulated state changes. */ void onUpdateEnd() { if (bulbStateUpdater) { bulbStateUpdater->enable(); } } /** * Apply what's in the Settings object. */ void applySettings() { if (milightClient) { delete milightClient; } if (mqttClient) { delete mqttClient; delete bulbStateUpdater; mqttClient = NULL; bulbStateUpdater = NULL; } if (stateStore) { delete stateStore; } if (packetSender) { delete packetSender; } if (radios) { delete radios; } transitions.setDefaultPeriod(settings.defaultTransitionPeriod); radioFactory = MiLightRadioFactory::fromSettings(settings); if (radioFactory == NULL) { Serial.println(F("ERROR: unable to construct radio factory")); } stateStore = new GroupStateStore(MILIGHT_MAX_STATE_ITEMS, settings.stateFlushInterval); radios = new RadioSwitchboard(radioFactory, stateStore, settings); packetSender = new PacketSender(*radios, settings, onPacketSentHandler); milightClient = new MiLightClient( *radios, *packetSender, stateStore, settings, transitions ); milightClient->onUpdateBegin(onUpdateBegin); milightClient->onUpdateEnd(onUpdateEnd); if (settings.mqttServer().length() > 0) { mqttClient = new MqttClient(settings, milightClient); mqttClient->begin(); mqttClient->onConnect([]() { if (settings.homeAssistantDiscoveryPrefix.length() > 0) { HomeAssistantDiscoveryClient discoveryClient(settings, mqttClient); discoveryClient.sendDiscoverableDevices(settings.groupIdAliases); discoveryClient.removeOldDevices(settings.deletedGroupIdAliases); settings.deletedGroupIdAliases.clear(); } }); bulbStateUpdater = new BulbStateUpdater(settings, *mqttClient, *stateStore); } initMilightUdpServers(); if (discoveryServer) { delete discoveryServer; discoveryServer = NULL; } if (settings.discoveryPort != 0) { discoveryServer = new MiLightDiscoveryServer(settings); discoveryServer->begin(); } // update LED pin and operating mode if (ledStatus) { ledStatus->changePin(settings.ledPin); ledStatus->continuous(settings.ledModeOperating); } // WiFi.hostname(settings.hostname); // WiFiPhyMode_t wifiMode; // switch (settings.wifiMode) { // case WifiMode::B: // wifiMode = WIFI_PHY_MODE_11B; // break; // case WifiMode::G: // wifiMode = WIFI_PHY_MODE_11G; // break; // default: // case WifiMode::N: // wifiMode = WIFI_PHY_MODE_11N; // break; // } // WiFi.setPhyMode(wifiMode); } /** * */ bool shouldRestart() { if (! settings.isAutoRestartEnabled()) { return false; } return settings.getAutoRestartPeriod()*60*1000 < millis(); } // give a bit of time to update the status LED void handleLED() { ledStatus->handle(); } void wifiExtraSettingsChange() { settings.wifiStaticIP = wifiStaticIP->getValue(); settings.wifiStaticIPNetmask = wifiStaticIPNetmask->getValue(); settings.wifiStaticIPGateway = wifiStaticIPGateway->getValue(); settings.save(); } // Called when a group is deleted via the REST API. Will publish an empty message to // the MQTT topic to delete retained state void onGroupDeleted(const BulbId& id) { if (mqttClient != NULL) { mqttClient->sendState( *MiLightRemoteConfig::fromType(id.deviceType), id.deviceId, id.groupId, "" ); } } void setup() { Serial.begin(115200); String ssid = "ESP" + String(getESPId()); // load up our persistent settings from the file system #ifdef ESP8266 SPIFFS.begin(); #elif ESP32 if(!SPIFFS.begin(true)){ Serial.println(F("Error while mounting SPIFFS")); } #endif Settings::load(settings); #ifdef ESP8266 applySettings(); #endif // set up the LED status for wifi configuration ledStatus = new LEDStatus(settings.ledPin); ledStatus->continuous(settings.ledModeWifiConfig); // start up the wifi manager if (! MDNS.begin("milight-hub")) { Serial.println(F("Error setting up MDNS responder")); } // tell Wifi manager to call us during the setup. Note that this "setSetupLoopCallback" is an addition // made to Wifi manager in a private fork. As of this writing, WifiManager has a new feature coming that // allows the "autoConnect" method to be non-blocking which can implement this same functionality. However, // that change is only on the development branch so we are going to continue to use this fork until // that is merged and ready. #ifdef ESP8266 wifiManager.setSetupLoopCallback(handleLED); #elif ESP32 // TODO check if the non-blocking implementation can be used or create a version with setSetupLoopCallback #endif // Allows us to have static IP config in the captive portal. Yucky pointers to pointers, just to have the settings carry through wifiManager.setSaveConfigCallback(wifiExtraSettingsChange); wifiStaticIP = new WiFiManagerParameter( "staticIP", "Static IP (Leave blank for dhcp)", settings.wifiStaticIP.c_str(), MAX_IP_ADDR_LEN ); wifiManager.addParameter(wifiStaticIP); wifiStaticIPNetmask = new WiFiManagerParameter( "netmask", "Netmask (required if IP given)", settings.wifiStaticIPNetmask.c_str(), MAX_IP_ADDR_LEN ); wifiManager.addParameter(wifiStaticIPNetmask); wifiStaticIPGateway = new WiFiManagerParameter( "gateway", "Default Gateway (optional, only used if static IP)", settings.wifiStaticIPGateway.c_str(), MAX_IP_ADDR_LEN ); wifiManager.addParameter(wifiStaticIPGateway); // We have a saved static IP, let's try and use it. if (settings.wifiStaticIP.length() > 0) { Serial.printf_P(PSTR("We have a static IP: %s\n"), settings.wifiStaticIP.c_str()); IPAddress _ip, _subnet, _gw; _ip.fromString(settings.wifiStaticIP); _subnet.fromString(settings.wifiStaticIPNetmask); _gw.fromString(settings.wifiStaticIPGateway); wifiManager.setSTAStaticIPConfig(_ip,_gw,_subnet); } wifiManager.setConfigPortalTimeout(180); if (wifiManager.autoConnect(ssid.c_str(), "milightHub")) { // set LED mode for successful operation ledStatus->continuous(settings.ledModeOperating); Serial.println(F("Wifi connected succesfully\n")); // if the config portal was started, make sure to turn off the config AP WiFi.mode(WIFI_STA); #ifdef ESP32 applySettings(); #endif } else { // set LED mode for Wifi failed ledStatus->continuous(settings.ledModeWifiFailed); Serial.println(F("Wifi failed. Restarting in 10 seconds.\n")); delay(10000); ESP.restart(); } MDNS.addService("http", "tcp", 80); #ifdef ESP8266 SSDP.setSchemaURL("description.xml"); SSDP.setHTTPPort(80); SSDP.setName("ESP8266 MiLight Gateway"); SSDP.setSerialNumber(ESP.getChipId()); SSDP.setURL("/"); SSDP.setDeviceType("upnp:rootdevice"); SSDP.begin(); #elif ESP32 // TODO SSDP #endif httpServer = new MiLightHttpServer(settings, milightClient, stateStore, packetSender, radios, transitions); httpServer->onSettingsSaved(applySettings); httpServer->onGroupDeleted(onGroupDeleted); #ifdef ESP8266 httpServer->on("/description.xml", HTTP_GET, []() { SSDP.schema(httpServer->client()); }); #elif ESP32 // TODO SSDP #endif httpServer->begin(); transitions.addListener( [](const BulbId& bulbId, GroupStateField field, uint16_t value) { StaticJsonDocument<100> buffer; const char* fieldName = GroupStateFieldHelpers::getFieldName(field); buffer[fieldName] = value; milightClient->prepare(bulbId.deviceType, bulbId.deviceId, bulbId.groupId); milightClient->update(buffer.as()); } ); Serial.printf_P(PSTR("Setup complete (version %s)\n"), QUOTE(MILIGHT_HUB_VERSION)); } void loop() { httpServer->handleClient(); if (mqttClient) { mqttClient->handleClient(); bulbStateUpdater->loop(); } for (size_t i = 0; i < udpServers.size(); i++) { udpServers[i]->handleClient(); } if (discoveryServer) { discoveryServer->handleClient(); } handleListen(); stateStore->limitedFlush(); packetSender->loop(); // update LED with status ledStatus->handle(); transitions.loop(); if (shouldRestart()) { Serial.println(F("Auto-restart triggered. Restarting...")); ESP.restart(); } } #endif