/** * @file watchMain.cpp * @author Gustice * @brief ChaosWatch Application */ /* Watchface 6/ 5 / 1 4 2 3 */ // Profiles = 5 (0..4) // DynRange = // MinSteps (6..20), MaxSteps (10..40) // activeState.accel = (min .5 / max 3) // activeState.speed = (min .5 / max 3) #include "watchMain.hpp" #include "config.hpp" using namespace spit; string ClientName = "CatasWatchDev"; string ProjectName = "CatastrphicProj"; extern "C" { // This switch allows the ROS C-implementation to find this main void app_main(void); } struct DeviceState { uint8_t minJumps{6}; uint8_t maxJumps{32}; uint8_t profile{}; uint8_t shuffleRate{}; float speed = 1.0; // Rad/sec float accel = 1.0; // Rad/sec^2 Color color{0x80, 0x80, 0x80}; }; enum class MoveType { StepTo, Demo, Shuffle, }; enum class ResponseType { StepAt, StartMoving, }; struct MoveCommand { MoveType type; // 1..6 uint8_t pos; }; struct WatchIndex { ResponseType type; // 1..6 uint8_t pos; }; struct PictoIllumCommand { uint8_t pos; // 0 for none / Valid indexes 1 .. 6 }; static fos::Queue commandQueue(8); static fos::Queue responseQueue(8); static fos::Queue pictoIllumQue(2); DeviceState activeState; static const char *TAG = "Watch"; bool EthernetIsConnected = false; std::unique_ptr pShell; void onCommand(std::string name, Cli::CArgs args) { ESP_LOGI(TAG, "Invoking: %s", name.c_str()); } void startUpdate(Cli::CArgs args) { ESP_LOGI(TAG, "Invoking Update"); if (args.size() != 1) { pShell->writeLine("Need URL to application"); return; } // @ToDo: Start Update here } void getConfig(Cli::CArgs args) { if (args.size() != 1) { pShell->writeLine("Needs string key for query"); return; } auto key = args[0]; auto &store = NvsApi::getInstance(); auto val = store.getValue(key); pShell->writeLine(key + ": " + val + ""); } void testPictoIllum(Cli::CArgs args) { if (args.size() != 1) { pShell->writeLine("Needs position argument for illuminated position 0 for off, 1..6 for pictograms"); return; } try { int p = std::stoi(args[0]); pictoIllumQue.enqueue(std::make_unique(p)); } catch (const std::exception &e) { ESP_LOGI(TAG, "Failed to parse %s", args[0].c_str()); } } void testMovement(Cli::CArgs args) { ESP_LOGI(TAG, "on triggerMovement:"); if (args.size() < 1) { pShell->writeLine("need target position (1..6)"); pShell->writeLine("additional optional arguments: Profile (0..4), MinSteps (6..20), MaxSteps (10..40)"); return; } try { int p = std::stoi(args[0]); if (args.size() >= 2) { activeState.profile = std::min(4, std::stoi(args[1])); } if (args.size() >= 3) { activeState.minJumps = std::min(20, std::stoi(args[2])); } if (args.size() >= 4) { activeState.minJumps = std::min(40, std::stoi(args[3])); } commandQueue.enqueue(std::make_unique(MoveType::StepTo, p)); } catch (const std::exception &e) { ESP_LOGI(TAG, "Failed to parse %s", args[0].c_str()); } } void testFeedback(Cli::CArgs args) { ESP_LOGI(TAG, "on triggerMovement:"); if (args.size() != 1) { pShell->writeLine("need target position (1..6) or 0 for moving"); return; } try { int p = std::stoi(args[0]); if (p == 0) { responseQueue.enqueue(std::make_unique(ResponseType::StartMoving, 0)); } else { responseQueue.enqueue(std::make_unique(ResponseType::StepAt, p)); } } catch (const std::exception &e) { ESP_LOGI(TAG, "Failed to parse %s", args[0].c_str()); } } void setConfig(Cli::CArgs args) { if (args.size() != 2) { pShell->writeLine("Needs string key and value"); return; } auto key = args[0]; auto val = args[1]; auto &store = NvsApi::getInstance(); pShell->writeLine(std::string{"Storing value = '"} + val + "'"); store.setValue(key, val); } void setLogServerConfig(Cli::CArgs args) { ESP_LOGI(TAG, "on showLogServerConfig:"); auto &store = NvsApi::getInstance(); if (args.size() != 1) { auto logger = store.getValue("syslogDst"); pShell->writeLine(std::string{"syslog destination = '"} + logger + "'"); pShell->writeLine("provide argument to change syslog server"); return; } pShell->writeLine(std::string{"setting syslog destination to = '"} + args[0] + "'"); store.setValue("syslogDst", args[0]); } void setDynamics(Cli::CArgs args) { ESP_LOGI(TAG, "on setDynamics:"); if (args.size() != 2) { pShell->writeLine("need float values for velocity and acceleration"); return; } try { float v = std::stof(args[0]); float a = std::stof(args[1]); activeState.accel = a; activeState.speed = v; } catch (const std::exception &e) { ESP_LOGI(TAG, "Failed to parse %s and %s", args[0].c_str(), args[1].c_str()); } } void showProcessValues(Cli::CArgs args) { pShell->writeLine("Process values:"); char buff[256]; sprintf(buff, "%02x,%02x,%02x", activeState.color.getRed(), activeState.color.getGreen(), activeState.color.getBlue()); pShell->writeLine(std::string{" Color RGB: "} + buff); sprintf(buff, "%.2f / %.2f", activeState.accel, activeState.speed); pShell->writeLine(std::string{" Accel/Speed: "} + buff); pShell->writeLine(std::string{" JumpRange: "} + std::to_string((int)activeState.minJumps) + " .. " + std::to_string((int)activeState.maxJumps)); pShell->writeLine(std::string{" Provile: "} + std::to_string((int)activeState.profile)); } void setSyslogLevel(Cli::CArgs args) { if (args.size() != 1) { pShell->writeLine("Needs SyslogLevel Warning = 4, Notice = 5, Informational = 6, Debug = 7"); return; } try { auto lvl = std::stoi(args[0]); if (lvl >= (int)Syslog::Severity::Warning && lvl <= (int)Syslog::Severity::Debug) { Syslog::setSeverity((Syslog::Severity)lvl); } else { pShell->writeLine("New level was not accepted"); } } catch (const std::exception &e) { ESP_LOGI(TAG, "Failed to parse %s", args[0].c_str()); } } /** * @brief Telnet socket via TCP/IP * * @details The ini-file for TeraTerm only changes CRReceive and CRSend values both to "=CR" * @param arg ignored task argument */ static void telnetSocketTask(int addrFamily) { ESP_LOGI(TAG, "Starting Telnet task"); try { TelnetServer telnet(addrFamily); telnet.run( [](TcpSession &session) { pShell = std::make_unique(session, [](std::string n, Cli::CArgs a) { onCommand(n, a); }); pShell->addBinding( {"update", "Start OTA-Update-Process", [](Cli::CArgs a) { startUpdate(a); }}); pShell->addBinding( {"setLogserver", "Set log server config ip", [](Cli::CArgs a) { setLogServerConfig(a); }}); pShell->addBinding( {"setLogLevel", "Set severity log server", [](Cli::CArgs a) { setSyslogLevel(a); }}); pShell->addBinding( {"getcfg", "Get configuration value for key", [](Cli::CArgs a) { getConfig(a); }}); pShell->addBinding( {"setcfg", "Set configuration value by key and value", [](Cli::CArgs a) { setConfig(a); }}); pShell->addBinding( {"setDyn", "set dynamics of movement (velocity,accelerations) as floats", [](Cli::CArgs a) { setDynamics(a); }}); pShell->addBinding( {"tstMove", "start movement (profile-idx,steps) as integers ", [](Cli::CArgs a) { testMovement(a); }}); pShell->addBinding( {"tstIllum", "test illumination command 0=off /1..6", [](Cli::CArgs a) { testPictoIllum(a); }}); pShell->addBinding( {"tstFb", "test feedback-channel to control 0=Move /1..6=FinalPos", [](Cli::CArgs a) { testFeedback(a); }}); pShell->addBinding( {"showData", "Show process values that are input by DMX, MQTT and SPIT", [](Cli::CArgs a) { showProcessValues(a); }}); }, [](TcpSession &session) { pShell->process(); Syslog::setupTask(session.getOther()); }); } catch (const std::exception &e) { ESP_LOGE("TCP", "%s", e.what()); } vTaskDelete(NULL); } constexpr uint32_t DmxUniverse = 0; constexpr int DmxChannelCount = 9; uint8_t scaledRgbChannel(uint8_t value, float scale) { return static_cast(value * scale); } /** * @brief Callback for ArtNet DMX-socket. Does not send data only updates the image * * @param data Pointer to dmx-values * @param size length of dmx-values */ static void UniverseCallback(const uint8_t *data, const uint16_t size) { int len = std::min(static_cast(size), DmxChannelCount); ESP_LOGD(TAG, "Got DMX"); ESP_LOG_BUFFER_HEXDUMP("DMX", data, len, ESP_LOG_DEBUG); Syslog::logv(Syslog::Severity::Debug, "Got DMX: r%0x g%0x b%0x i%0x m%i M%i p%i a%i w%i r%i", /* RGB*/ data[0], data[1], data[2], /*Intens*/ data[3], /* MinMaxJumps */ data[4], data[5], /*profile*/ data[6], /*accel/speed*/ data[7], data[8], /*shuffleRate*/ data[9]); float intens = (float)data[3] / UINT8_MAX; activeState.color = Color{ scaledRgbChannel(data[0], intens), scaledRgbChannel(data[1], intens), scaledRgbChannel(data[2], intens), 0}; activeState.minJumps = data[4]; activeState.maxJumps = data[5]; activeState.profile = data[6]; uint8_t accel = data[7]; uint8_t velo = data[8]; activeState.accel = (float)accel / 100 + 0.5; // min .5 / max 3 activeState.speed = (float)velo / 100 + 0.5; // min .5 / max 3 activeState.shuffleRate = data[9]; } void moveToSubs(std::string data) { ESP_LOGI(TAG, "MoveTo command '%s'", data.c_str()); try { int p = std::stoi(data); Syslog::logv(Syslog::Severity::Informational, "got MoveTo-Cmd->%i", p); commandQueue.enqueue(std::make_unique(MoveType::StepTo, p)); } catch (const std::exception &e) { ESP_LOGE(TAG, "Error to process data %s, with ex: %s", data.c_str(), e.what()); } } void startShuffle(std::string data) { ESP_LOGI(TAG, "StartShuffle command %s", data.c_str()); try { Syslog::logv(Syslog::Severity::Informational, "got Shuffle-Command"); commandQueue.enqueue(std::make_unique(MoveType::Shuffle, 0)); } catch (const std::exception &e) { ESP_LOGE(TAG, "Error to process data %s, with ex: %s", data.c_str(), e.what()); } } /** * @brief DMX-Socket for ArtNet TCP/IP * * @details Tested with [Q Light Controller+](https://www.qlcplus.org/) * @param args ignored task argument */ static void dmxSocketSenderTask(UdpSocket &socket) { try { ArtnetSender artnet(socket); std::array data; while (socket.isActive()) { data[0]++; artnet.send("", 0, data.data(), data.size()); fos::Task::delayMs(500); } } catch (const std::exception &e) { ESP_LOGE("UDP", "%s", e.what()); } } /** * @brief DMX-Socket for ArtNet TCP/IP * * @details Tested with [Q Light Controller+](https://www.qlcplus.org/) * @param args ignored task argument */ static void dmxSocketTask(int addrFamily) { ESP_LOGI(TAG, "Starting ArtNet task"); constexpr uint16_t ArtNetPort = arx::artnet::DEFAULT_PORT; IpAdr address = addrFamily == AF_INET ? IpAdr(INADDR_ANY, ArtNetPort) : IpAdr(ArtNetPort); try { while (true) { UdpSocket socket(address); socket.bindTo(); ArtnetReceiver artnet(socket); artnet.subscribe(DmxUniverse, UniverseCallback); artnet.parse(); // parse artnet as long socket is active, then restart socket while (socket.isActive()) { ESP_LOGD(TAG, "Waiting for data"); artnet.parse(); } } } catch (const std::exception &e) { ESP_LOGE("UDP", "%s", e.what()); } } fos::Semaphore etherActive; fos::Semaphore spitProjectFinish; /** Event handler for IP_EVENT_ETH_GOT_IP */ void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; const esp_netif_ip_info_t *ip_info = &event->ip_info; ESP_LOGI(TAG, "Ethernet Got IP Address"); ESP_LOGI(TAG, "~~~~~~~~~~~"); ESP_LOGI(TAG, "ETHIP:" IPSTR, IP2STR(&ip_info->ip)); ESP_LOGI(TAG, "ETHMASK:" IPSTR, IP2STR(&ip_info->netmask)); ESP_LOGI(TAG, "ETHGW:" IPSTR, IP2STR(&ip_info->gw)); ESP_LOGI(TAG, "~~~~~~~~~~~"); etherActive.give(); } static void initialize_ethernet(void) { uint8_t eth_port_cnt = 0; esp_eth_handle_t *eth_handles; ESP_ERROR_CHECK(eth_init(ð_handles, ð_port_cnt)); if (eth_port_cnt > 1) { ESP_LOGW(TAG, "multiple Ethernet devices detected, the first initialized is to be used!"); } static esp_eth_handle_t s_eth_handle = NULL; s_eth_handle = eth_handles[0]; free(eth_handles); // Initialize TCP/IP network interface aka the esp-netif (should be called only once in application) ESP_ERROR_CHECK(esp_netif_init()); // Create default event loop that running in background ESP_ERROR_CHECK(esp_event_loop_create_default()); // Use ESP_NETIF_DEFAULT_ETH when just one Ethernet interface is used and you don't need to modify // default esp-netif configuration parameters. esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH(); esp_netif_t *eth_netif = esp_netif_new(&cfg); // Attach Ethernet driver to TCP/IP stack ESP_ERROR_CHECK(esp_netif_attach(eth_netif, esp_eth_new_netif_glue(s_eth_handle))); ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, eth_event_handler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_event_handler, NULL)); ESP_ERROR_CHECK(esp_eth_start(s_eth_handle)); } namespace Effect { extern const WaveForm_t OffsetPulseWave{gau8_offsetPulse, cu16_TemplateLength}; extern const WaveForm_t InitSlopeWave{gau8_initSlope, cu16_TemplateLength}; extern const WaveForm_t OffsetSlopeWave{gau8_offsetSlope, cu16_TemplateLength}; extern const WaveForm_t FullSlopeWave{gau8_fullSlope, cu16_TemplateLength}; extern const WaveForm_t FullPulseWave{gau8_fullPulse, cu16_TemplateLength}; const Macro macDemoStart = { {64, 1, &InitSlopeWave, StateMachine::UpdateWave, 0xFF}, {32, 1, StateMachine::UpdateIdle, 0xFF}, }; const Macro macDemoPulse = { {64, 1, &InitSlopeWave, StateMachine::UpdateWave, 0xFF}, {32, 2, StateMachine::UpdateIdle, 0xFF}, {64, 1, &OffsetPulseWave, StateMachine::UpdateWave, 0xFF}, }; const Macro macDemoFlicker = { {16, 1, &InitSlopeWave, StateMachine::UpdateWave, 0xFF}, {32, 1, StateMachine::UpdateFlicker, 0xFF}, }; } // namespace Effect void LightingTask() { ESP_LOGI(TAG, "Start Lighting"); static I2cMaster master(I2C_NUM_0, {/*sda=*/GPIO_NUM_10, /*scl=*/GPIO_NUM_11}); // static PmwExpanderPca9685 pca9685(master); static PmwExpanderPca9685 ledDriver1(master, /*iverted=*/false); static PmwExpanderPca9685 ledDriver2(master, /*iverted=*/false, 0x41); PmwExpanderPca9685::GrayValues values[2]{}; OutputPort pwmEnable(GPIO_NUM_1, GpioPort::Logic::Inverted); pwmEnable.write(true); auto const &colorSource = activeState.color; std::array processors{ Effect::EffectProcessor(8), Effect::EffectProcessor(8), Effect::EffectProcessor(8), Effect::EffectProcessor(8), Effect::EffectProcessor(8), Effect::EffectProcessor(8)}; processors[0].setEffect(Effect::macDark, &colorSource, 0); processors[1].setEffect(Effect::macDark, &colorSource, 0); processors[2].setEffect(Effect::macDark, &colorSource, 0); processors[3].setEffect(Effect::macDark, &colorSource, 0); processors[4].setEffect(Effect::macDark, &colorSource, 0); processors[5].setEffect(Effect::macDark, &colorSource, 0); const std::array PictoEffects{ Effect::macDemoPulse, // 1 Effect::macDemoPulse, Effect::macDemoPulse, Effect::macDemoPulse, Effect::macStartFlicker, Effect::macDemoPulse}; // 6 uint8_t lastPicto = 1; std::array startPoints{ &(values[0][4]), // 1 &(values[1][12]), &(values[1][8]), &(values[1][4]), &(values[0][12]), &(values[0][8]), // 6 }; while (true) { auto onPos = pictoIllumQue.tryDequeue(); if (onPos) { ESP_LOGI(TAG, "Changing picto-Illum to pos = %i", onPos->pos); ESP_LOGI(TAG, "Current color set r%i g%i b%i ", activeState.color.getRed(), activeState.color.getGreen(), activeState.color.getBlue()); auto pos = onPos->pos; if (pos == 0) { Syslog::logv(Syslog::Severity::Informational, "Dimming picto-illum pos:%i ", lastPicto); processors[lastPicto - 1].setEffect(Effect::macDark, &colorSource, 0); } else if ((pos >= 1) && (pos <= 6)) { auto idx = pos - 1; Syslog::logv(Syslog::Severity::Informational, "Firing picto-illum pos:%i ", pos); processors[idx].setEffect(PictoEffects[idx], &colorSource, 0); lastPicto = pos; } else { Syslog::logv(Syslog::Severity::Error, "Got invalid index for illuminated pictogram =%i ", pos); } } for (size_t i = 0; i < processors.size(); i++) { auto col = processors[i].tick(); auto sp = startPoints[i]; sp[0] = (uint16_t)(col.getWhite()) << 7; sp[1] = (uint16_t)(col.getBlue()) << 7; sp[2] = (uint16_t)(col.getGreen()) << 7; sp[3] = (uint16_t)(col.getRed()) << 7; } ledDriver1.send(values[0]); ledDriver2.send(values[1]); fos::Task::delayMs(40); } } void MovementTask() { ESP_LOGI(TAG, "Start Stepper"); constexpr int MicroSteps = 32; StepperDriverDrv8833::Pins stepperPins{ LEDC_TIMER_0, {LEDC_CHANNEL_0, LEDC_CHANNEL_1, LEDC_CHANNEL_2, LEDC_CHANNEL_3}, {GPIO_NUM_21, GPIO_NUM_20, GPIO_NUM_23, GPIO_NUM_22}, }; float power = 0.25F; // try // { // auto pwr = store.getValue(ConfigKeys.motorPower); // power = std::stof(pwr); // } // catch(...) // { // std::cerr << e.what() << '\n'; // ESP_LOGI("Stepper", "Applying default"); // } StepperDriverDrv8833 stepperDriver(stepperPins, MicroSteps, power); stepperDriver.setPos(0); StepperConfig stepperCfg{1.8F, MicroSteps}; InputPort initSens(GPIO_NUM_18); InputPort motorFault(GPIO_NUM_8); motorFault.setLogic(GpioPort::Logic::Inverted); OutputPort motorSleep(GPIO_NUM_15, GpioPort::Logic::Inverted); motorSleep.write(false); StepperMotor stepper(stepperCfg, stepperDriver, initSens); SyslogInst logger; WatchMotor watch(stepper, [](uint8_t newPos) { pictoIllumQue.enqueue(std::make_unique(newPos)); responseQueue.enqueue(std::make_unique(ResponseType::StepAt, newPos)); }, logger); ESP_LOGI("Stepper", "Find Home"); stepper.findRef(); // Todo Set by User-Interface for (size_t i = 0; i < 7; i++) { stepper.fullStep(1); fos::Task::delayMs(40); } (void)stepper.zeroPosition(); while (!stepper.isIdle()) { fos::Task::delayMs(40); } // watch.moveTo(1, ProfileGenerator::Unitform); ESP_LOGI("Stepper", "Got to 1"); // Illum 6 pictoIllumQue.enqueue(std::make_unique(0)); ESP_LOGI(TAG, "Fist Position is %.2f", watch.getScaled()); fos::Task::delayMs(1000); while (true) { ESP_LOGD(TAG, "Waiting for command"); auto cmd = commandQueue.dequeue(); if (cmd->type == MoveType::StepTo) { if (stepper.isIdle()) { int newPos = cmd->pos; if ((newPos >= 1) && (newPos <= 6)) { Syslog::logv(Syslog::Severity::Informational, "Triggered movement to picto pos:%i ", newPos); } else { Syslog::logv(Syslog::Severity::Error, "Got invalid index for illuminated pictogram =%i ", newPos); continue; } Syslog::logv(Syslog::Severity::Notice, "Running Stepper by min%i steps with prof=%i a=%.2f w=%.2f", activeState.minJumps, activeState.profile, activeState.accel, activeState.speed); pictoIllumQue.enqueue(std::make_unique(0)); responseQueue.enqueue(std::make_unique(ResponseType::StartMoving, 0)); auto &as = activeState; watch.moveTo(newPos, as.profile, as.minJumps, as.accel, as.speed); // pictoIllumQue.enqueue(std::make_unique(newPos)); // responseQueue.enqueue(std::make_unique(ResponseType::StepAt, newPos)); } } else if (cmd->type == MoveType::Shuffle) { Syslog::logv(Syslog::Severity::Informational, "Starting Shuffle mode"); while (true) { int pos = Random::randomWithIn(1,6); int profile = Random::randomWithIn(0,3); if (stepper.isIdle()) { Syslog::logv(Syslog::Severity::Notice, "Running Stepper to %i by min%i steps with prof=%i a=%.2f w=%.2f", pos, activeState.minJumps, activeState.profile, activeState.accel, activeState.speed); pictoIllumQue.enqueue(std::make_unique(0)); responseQueue.enqueue(std::make_unique(ResponseType::StartMoving, 0)); auto &as = activeState; watch.moveTo(pos, profile, as.minJumps, as.accel, as.speed); } // int delay = Random::randomWithIn(20000,100000)/portTICK_PERIOD_MS; int delay = Random::randomWithIn(2000,3000)/portTICK_PERIOD_MS; Syslog::logv(Syslog::Severity::Informational, "ShuffleMode Wait for %i", delay); auto cmd = commandQueue.tryDequeueFor(delay); if (cmd) { if (cmd->type != MoveType::Shuffle) { commandQueue.enqueue(std::move(cmd)); break; } } } } else if (cmd->type == MoveType::Demo) { ESP_LOGI(TAG, "Running Demo"); for (size_t i = ProfileGenerator::Smooth; i < ProfileGenerator::NumProfiles; i++) { ESP_LOGI(TAG, "Running Profile %d ", i); fos::Task::delayMs(1000); int steps[3]{4, 2, 0}; for (size_t j = 0; j < 3; j++) { watch.moveTo(steps[j], i, 1, 1, 1); fos::Task::delayMs(1000); } } } else { fos::Task::delayMs(100); } } } struct LocalSpitObj { string iD; string groupName; string name; }; std::map ProvidedSpitActions; std::map ExpectedObjects; void onServerEvent(SClient::ServerEvent event, const SadDetectedServer &p_DetectedServer) { switch (event) { case SClient::ServerEvent::Detect: ESP_LOGI(TAG, "SERVER DETECTED"); ESP_LOGI(TAG, " Name: %s", p_DetectedServer.getName().data()); ESP_LOGI(TAG, " ID: %s", p_DetectedServer.getID().data()); ESP_LOGI(TAG, " Computer: %s", p_DetectedServer.getComputerName().data()); ESP_LOGI(TAG, " IP: %s", p_DetectedServer.getIP().data()); ESP_LOGI(TAG, " Port: %s", p_DetectedServer.getPort().data()); ESP_LOGI(TAG, " Addition: %s", p_DetectedServer.getAdditions().data()); ESP_LOGI(TAG, " ClientIP: %s", p_DetectedServer.getClientIP().data()); break; case SClient::ServerEvent::Lost: ESP_LOGW(TAG, "Server lost Name: %s IP: %s", p_DetectedServer.getName().data(), p_DetectedServer.getIP().data()); break; case SClient::ServerEvent::Changed: ESP_LOGW(TAG, "Server lost Name: %s IP: %s", p_DetectedServer.getName().data(), p_DetectedServer.getIP().data()); break; default: break; } } void onConnectionStatus(string p_ServerID, string p_ServerComputerName, string p_ServerName, int p_ServerStepTime, SClient::Connection p_ConnectionStatus, SClient::Connection p_OldConnectionStatus) { ESP_LOGI(TAG, "MySPITApp: eventConnectionStatus %d->%d ", (int)p_OldConnectionStatus, (int)p_ConnectionStatus); } // The transfer of the server's SPITTypes and SPITObjects is finished. void onProjectUpdate(SClient::ProjUpdate state, string projectId) { switch (state) { case SClient::ProjUpdate::ServerLoaded: ESP_LOGI(TAG, "eventProjectReport ID: %s ", projectId.data()); break; case SClient::ProjUpdate::StartTransfer: ESP_LOGI(TAG, "eventStartProjectTransfer"); break; case SClient::ProjUpdate::EndTransfer: ESP_LOGI(TAG, "eventEndProjectTransfer"); spitProjectFinish.give(); break; case SClient::ProjUpdate::ServerClosed: ESP_LOGI(TAG, "eventProjectClosed ID: %s", projectId.data()); // If we don't save SPITServer's project specific information // perhaps close all SPITObjects and RemoteSPITObjects to avoid conflicts, when the SPITServer loads a new project // SPITObject::clearSPITObjects(); // SPITObject::clearSPITObjectsRemote(); break; default: break; } } bool onCheckSpitTypeVeto(bool knownType, bool createdByClient) { return true; // Bypass veto if (!knownType || !createdByClient) { // Don't allow foreign SPITTypes return false; } return true; } bool onCheckSpitObjectVeto(bool knownType) { return true; // Bypass veto if (!knownType) { // Don't allow objects of unregistered SPITTypes return false; } return true; } // Called when the server has created, loaded, changed or deleted a (Remote) SPITType. // Remote SPITType are used in Remote SPITObjects. A Remote SPITObject is used to remote control the SPITServer. // May also occur when the server confirms a SPITType created or changed by the client. void onSpitType(SClient::ObjectEvent event, bool isRemote, const SType &sType) { if (isRemote) { if (event == SClient::ObjectEvent::Created) { ESP_LOGI(TAG, "NEW SPIT-REMOTE-Type ID: %s GroupName: %s Name: %s confirmed: %d", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data(), sType.getConfirmed()); auto typeName = sType.Name.get(); if (ExpectedObjects.contains(typeName)) { ExpectedObjects[typeName] = sType.getID(); } } else if (event == SClient::ObjectEvent::Changed) { // we had to inform all Remote SPITObjects using the p_SPITType // perhaps the SPITObject uses information from the SPITType // SPITObject *ptrSPITObject; // int intSPITObjectCount; // intSPITObjectCount = SPITObject::getSPITObjectRemoteCount(); // for (int intObject = 0; intObject < intSPITObjectCount; intObject++) { // ptrSPITObject = SPITObject::getSPITObjectRemote(intObject); // if (ptrSPITObject == nullptr) // continue; // if (ptrSPITObject->getSPITTypeID() == p_SPITType.getID()) { // // call ecentSPITObjectRemote // // onSpitObjectRemote(SClient::ObjectEvent::Changed, p_SPITType, *ptrSPITObject); // } // } // ESP_LOGI(TAG, "SPIT-REMOTE-Type CHANGED ID: %s GroupName: %s Name: %s confirmed: %d", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data(), sType.getConfirmed()); } else if (event == SClient::ObjectEvent::Deleted) { ESP_LOGD(TAG, "SPIT-REMOTE-Type Removed ID: %s GroupName: %s Name: %s\n", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data()); } } else { if (event == SClient::ObjectEvent::Created) { // Whenever you provide an action for this SPITType take this over in your code. // call _SpitClient.setActionSPITType(sType.getID(), true); // ESP_LOGI(TAG, "NEW SPIT-Type ID: %s GroupName: %s Name: %s confirmed: %d", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data(), sType.getConfirmed()); } else if (event == SClient::ObjectEvent::Changed) { // ESP_LOGI(TAG, "SPIT-Type CHANGED ID: %s GroupName: %s Name: %s confirmed: %d", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data(), sType.getConfirmed()); } else if (event == SClient::ObjectEvent::Deleted) { // ESP_LOGI(TAG, "SPIT-Type Removed ID: %s GroupName: %s Name: %s\n", sType.getID().data(), sType.GroupName.get().data(), sType.Name.get().data()); } } } // Called when the server has created, loaded or changed a SPITObject. // A Remote SPITObject is used to remote control the SPITServer. // May also occur when the server confirms a SPITObject created by the client, what the client should never do!!. // false = server changed a existing SPITObject // @param p_SPITType a reference to a copy of the associated SPITType instance. // @param p_SPITObject a reference to a copy of the SPITObject instance. void onSpitObject(SClient::ObjectEvent event, bool isRemote, const SType &sType, const SObject &sObject) { if (isRemote) { if (event == SClient::ObjectEvent::Created) { // ESP_LOGI(TAG, "NEW SPIT-REMOTE-Object ID: %s Name: %s -> SPITType: %s", sObject.getID().data(), sObject.Name.get().data(), sType.Name.get().data()); // This is a remote SPITObject which can be used to remote control the server. // If you want you can list this for later use } else if (event == SClient::ObjectEvent::Changed) { // ESP_LOGI(TAG, "SPIT-REMOTE-Object CHANGED ID: %s Name: %s -> SPITType: %s", sObject.getID().data(), sObject.Name.get().data(), sType.Name.get().data()); } else if (event == SClient::ObjectEvent::Deleted) { ESP_LOGD(TAG, "SPIT-REMOTE-Object Removed ID: %s Name: %s\n", sObject.getID().data(), sObject.Name.get().data()); // This is a remote SPITObject // If you have listet this prior you should remove it from the list } } else { if (event == SClient::ObjectEvent::Created) { // ESP_LOGI(TAG, "NEW SPIT-Object ID: %s Name: %s -> SPITType: %s", sObject.getID().data(), sObject.Name.get().data(), sType.Name.get().data()); // check your actions // Whenever you provide an action for this SPITObject take this over in your code. // call _SpitClient.setActionSPITObject(sObject.getID(), true); // for our little excample as described in loadClientProject: // We have only actions for SPITTypes - means every SPITObject with the associated "Actionx"-SPITTypes will trigger the action when it is played //-> no need to do something here, because setActionSPITType has been set prior and the SPITObject will automatically inherits this // SPITObjects with other SPITTypes asscciated will trigger no actions } else if (event == SClient::ObjectEvent::Changed) { // ESP_LOGI(TAG, "SPIT-Object CHANGED ID: %s Name: %s -> SPITType: %s", sObject.getID().data(), sObject.Name.get().data(), sType.Name.get().data()); } else if (event == SClient::ObjectEvent::Deleted) { ESP_LOGD(TAG, "SPIT-Object Removed ID: %s Name: %s\n", sObject.getID().data(), sObject.Name.get().data()); // Check your actions // take the removal of the SPITObject over in your code // CAUTION: Deleted SPITObjects can be added later again (perhaps per UNDO on the SPITServer) //-> don't remove your assigned action immediately - keep it to reassign it later } } } // SPITServer may use several methods for playing SPITObjects. // e.g. liveSHOWsoftware has the timeline and jingles which plays objects, where the timeline the main player is. // SPIT distinguishes between the different players. All eventPlay... functions contain the information which player plays the SPIT object. void onPlay(string p_PlayerID, string p_PlayerName, char p_PlayerType, bool p_PlayerIsRunning, uint64_t p_TimeFrames) { ESP_LOGD(TAG, "eventPlay Player: %s Runs: %d Time: %lld", p_PlayerName.data(), p_PlayerIsRunning, p_TimeFrames); switch (p_PlayerType) { case SClient::PlayerType::PTYPE_MAIN: // p_FrameTimes contains the position inside the server's project, we can use it like a timecode // int intMilliseconds = (int)(((double)p_TimeFrames / (double)m_ServerFramesPerSecond) * 1000); break; case SClient::PlayerType::PTYPE_CONTINOUS: // The olayer is playing only single Object sequences sometime during the project break; case SClient::PlayerType::PTYPE_DISCRETE: // Only discrete values where submitted, e.g the fader position of remote faders. break; } } /** * Called when the SPITServer plays a SPITObject. For every played SPITObject one eventPlayObject is called. * You can decide here which action should be done or not. */ void onPlaySpitType(string p_PlayerID, string spitTypeId, bool p_PlayerIsRunning, bool p_Started, bool p_Ended, double p_Value) { // double doubleValue = ((double)p_PlayObject.getValue() / 1000000) * ((double)p_PlayObject.getValueFactor() / 1000000); ESP_LOGD(TAG, "eventPlayObject Player: %s is running: %d Object Started: %d Stopped: %d", p_PlayerID.data(), p_PlayerIsRunning, p_Started, p_Ended); // ESP_LOGD(TAG, " SPITType: %s SPITObject: %s Position: %lld Value: %f\n", spitTypeId.data(), p_SPITObject.getName().data(), p_PlayObject.getFrameInside(), doubleValue); ESP_LOGD(TAG, " SPITType: %s Value: %f\n", spitTypeId.data(), p_Value); if (!ProvidedSpitActions.contains(spitTypeId)) { ESP_LOGE(TAG, "SPIT ID %s is not accepted", spitTypeId.c_str()); return; } if (p_Started == true) { auto command = ProvidedSpitActions[spitTypeId]; // ESP_LOGI(TAG, "Player: %s is running: %s Object", p_PlayerID.data(), spitTypeId.c_str()); commandQueue.enqueue(std::make_unique(command)); Syslog::logv(Syslog::Severity::Informational, "Got SPIT command for move ot : r%d", command.pos); } } /** * Called when a remote SpitObject which value function is changed by the server. * This allows the Client to adjust its display for the remote command. * @param p_SPITType the assigned remote SPITType of p_SPITObject * @param p_SPITObject the remote SPITObject which is changed * @param p_FrameInsideObject can be discarded * @param p_Value the new value */ void onPlayRemoteObjectStatus(const SType &sType, const SObject &sObject, uint64_t frameInsideObject, int value) { ESP_EARLY_LOGD(TAG, "eventPlayRemoteObjectStatus RemoteSPITType: %s RemoteSPITObject: %s Position: %lld Value: %d", sType.Name.get().data(), sObject.Name.get().data(), frameInsideObject, value); } void SpitClientApp() { ESP_LOGI(TAG, "Starting SPIT task"); auto &provided = SpitTypes.Provided; auto &expected = SpitTypes.Expected; std::vector ProvidedObj; ProvidedSpitActions[provided.startShuffle] = MoveCommand{.type = MoveType::Shuffle, .pos = 0}; ProvidedObj.push_back(LocalSpitObj{.iD = provided.startShuffle, .groupName = provided.group, .name = provided.startShuffle}); ExpectedObjects[expected.changingPos] = ""; for (size_t i = 0; i < 6; i++) { ProvidedSpitActions[provided.moveToX[i]] = MoveCommand{.type = MoveType::StepTo, .pos = (uint8_t)(i + 1)}; ProvidedObj.push_back(LocalSpitObj{.iD = provided.moveToX[i], .groupName = provided.group, .name = provided.moveToX[i]}); ExpectedObjects[expected.inPosX[i]] = ""; } auto ip = spit::Socket::getAdaptersInfo(); spit::Socket socket(ip); SClient spitClient(ClientName, ProjectName, socket, { .onServerEvent = onServerEvent, .onConnectionStatus = onConnectionStatus, .onProjectUpdate = onProjectUpdate, .onSpitType = onSpitType, .onSpitObject = onSpitObject, .onPlay = onPlay, .onPlaySpitType = onPlaySpitType, .onPlayRemoteObjectStatus = onPlayRemoteObjectStatus, .onCheckSpitTypeVeto = onCheckSpitTypeVeto, .onCheckSpitObjectVeto = onCheckSpitObjectVeto, }); for (auto &obj : ProvidedObj) { SType *ptrSPITType1Fix = spitClient.createTypeFixed(obj.iD, obj.groupName, obj.name, 0, 0); spitClient.setActionType(ptrSPITType1Fix->getID(), true); // should be executed whenever a SPITObject with this SPITType is played } spitClient.clientProjectLoaded(); spitClient.startServerAutoDetect(); spitProjectFinish.take(); string serverProjectID = spitClient.getServerProjectID(); string serverProjectName = spitClient.getClientProjectName(); int serverFramesPerSecond = spitClient.getServerFramesPerSecond(); // set when the server reports his project, see eventProjectReport ESP_LOGI(TAG, "eventProjectReport ID: %s Name: %s FramesPerSecond: %d", serverProjectID.data(), serverProjectName.data(), serverFramesPerSecond); for (auto &&obj : ExpectedObjects) { if (obj.second == "") { ESP_LOGE(TAG, "No matching Remote ID for %s", obj.first.c_str()); } } while (true) { auto res = responseQueue.dequeue(); if (res->type == ResponseType::StartMoving) { spitClient.sendRemoteObject(ExpectedObjects[expected.changingPos]); } else if (res->type == ResponseType::StepAt) { spitClient.sendRemoteObject(ExpectedObjects[expected.inPosX[res->pos - 1]]); } } } void MqttThread() { ESP_LOGI(TAG, "Starting MQTT task"); auto &store = NvsApi::getInstance(); auto broker = store.getValue(ConfigKeys.mqttBroker); if (broker != "") { ESP_LOGI(TAG, "Initializing MQTT"); auto prefix = store.getValue(ConfigKeys.mqttPrefix); auto &client = MqttClient::init(broker, prefix); if (!client.isRunning()) { client.addSubscription(MqttTopics.moveTo, [](std::string d) { moveToSubs(d); }); client.addSubscription(MqttTopics.startShuffle, [](std::string d) { startShuffle(d); }); client.start(); } while (true) { auto res = responseQueue.dequeue(); if (res->type == ResponseType::StartMoving) { client.publish(MqttTopics.changingPosition, ""); } else if (res->type == ResponseType::StepAt) { client.publish(MqttTopics.inPosition, std::to_string(res->pos)); } } } } void app_main(void) { initialize_ethernet(); NvsApi::init(); static fos::Task lightingTask(LightingTask, "Lights", 0x8000uL); static fos::Task movementTask(MovementTask, "Motor", 0x8000uL, fos::Task::MAX_PRIORITY-4 ); etherActive.take(); fos::Task::delayMs(10000); ESP_LOGI(TAG, "Start net services"); static int ipVersion = AF_INET; static fos::TaskT telnetSocket(telnetSocketTask, ipVersion, "tcpServer", 4096); static fos::TaskT dmxNode(dmxSocketTask, ipVersion, "dmxNode", 4096); static fos::Task spit(SpitClientApp, "SpitClient"); // static fos::Task mqtt(MqttThread, "MqttEndpoint"); // static Task otaUpdate(SystemFunctions::OtaTaskEntry, "otaTask", 8192); auto &store = NvsApi::getInstance(); auto logger = store.getValue(ConfigKeys.syslogDest); if (logger != "") { Syslog::setupTask(IpAdr{logger, 0}); } ESP_LOGI(TAG, "Configuration"); ESP_LOGI(TAG, "%s: %s", ConfigKeys.syslogDest, store.getValue(ConfigKeys.syslogDest).c_str()); ESP_LOGI(TAG, "%s: %s", ConfigKeys.mqttBroker, store.getValue(ConfigKeys.mqttBroker).c_str()); ESP_LOGI(TAG, "%s: %s", ConfigKeys.mqttPrefix, store.getValue(ConfigKeys.mqttPrefix).c_str()); ESP_LOGI(TAG, "%s: %s", ConfigKeys.spitServer, store.getValue(ConfigKeys.spitServer).c_str()); while (true) { fos::Task::delayMs(5000); // ToDo send Syslog::logv(Syslog::Severity::Notice, "Min heap size: %u / FreeHeap %lu", heap_caps_get_minimum_free_size(MALLOC_CAP_DEFAULT), esp_get_free_heap_size()); } }