diff --git a/CMakeLists.txt b/CMakeLists.txt index c36781c..1bac189 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) project("AOG-TaskController") set(PROJECT_VERSION_MAJOR 1) -set(PROJECT_VERSION_MINOR 2) +set(PROJECT_VERSION_MINOR 3) set(PROJECT_VERSION_PATCH 0) set(PROJECT_VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" @@ -29,7 +29,7 @@ FetchContent_MakeAvailable(Boost) FetchContent_Declare( isobus GIT_REPOSITORY https://github.com/Open-Agriculture/AgIsoStack-plus-plus.git - GIT_TAG 495eba6653010449d6202165240da7623243f416 + GIT_TAG 05e5fd73315d79a48c21c24f39350063a1556af6 DOWNLOAD_EXTRACT_TIMESTAMP TRUE) FetchContent_MakeAvailable(isobus) diff --git a/include/app.hpp b/include/app.hpp index f1b782a..784984e 100644 --- a/include/app.hpp +++ b/include/app.hpp @@ -35,6 +35,7 @@ class Application std::shared_ptr canDriver; std::shared_ptr tcServer; + std::shared_ptr tecuCF = nullptr; std::unique_ptr speedMessagesInterface; std::unique_ptr nmea2000MessageInterface; std::uint8_t nmea2000SequenceIdentifier = 0; diff --git a/include/task_controller.hpp b/include/task_controller.hpp index 4e7685b..07e2312 100644 --- a/include/task_controller.hpp +++ b/include/task_controller.hpp @@ -9,6 +9,7 @@ #pragma once +#include "isobus/isobus/isobus_data_dictionary.hpp" #include "isobus/isobus/isobus_device_descriptor_object_pool.hpp" #include "isobus/isobus/isobus_standard_data_description_indices.hpp" #include "isobus/isobus/isobus_task_controller_server.hpp" @@ -48,6 +49,9 @@ class ClientState void mark_measurement_commands_sent(); std::uint16_t get_element_number_for_ddi(isobus::DataDescriptionIndex ddi) const; void set_element_number_for_ddi(isobus::DataDescriptionIndex ddi, std::uint16_t elementNumber); + // Element work state management these act like master / override for actual sections + void set_element_work_state(std::uint16_t elementNumber, bool isWorking); + bool try_get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const; private: isobus::DeviceDescriptorObjectPool pool; ///< The device descriptor object pool (DDOP) for the TC @@ -59,6 +63,7 @@ class ClientState std::vector sectionActualStates; // 2 bits per section (0 = off, 1 = on, 2 = error, 3 = not installed) bool setpointWorkState = false; ///< The overall work state desired bool actualWorkState = false; ///< The overall work state actual + std::map elementWorkStates; ///< Work state per element (element number -> is working) bool isSectionControlEnabled = false; ///< Stores auto vs manual mode setting }; diff --git a/src/app.cpp b/src/app.cpp index ef3b463..39932e8 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -41,33 +41,64 @@ bool Application::initialize() return false; } + isobus::CANNetworkManager::CANNetwork.get_configuration().set_number_of_packets_per_cts_message(255); + isobus::NAME ourNAME(0); //! Make sure you change these for your device!!!! ourNAME.set_arbitrary_address_capable(true); ourNAME.set_industry_group(2); ourNAME.set_device_class(0); - ourNAME.set_function_code(static_cast(isobus::NAME::Function::TaskController)); ourNAME.set_identity_number(20); ourNAME.set_ecu_instance(0); ourNAME.set_function_instance(0); // TC #1. If you want to change the TC number, change this. ourNAME.set_device_class_instance(0); ourNAME.set_manufacturer_code(1407); - auto serverCF = isobus::CANNetworkManager::CANNetwork.create_internal_control_function(ourNAME, 0, isobus::preferred_addresses::IndustryGroup2::TaskController_MappingComputer); // The preferred address for a TC is defined in ISO 11783 - auto addressClaimedFuture = std::async(std::launch::async, [&serverCF]() { - while (!serverCF->get_address_valid()) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); }); + isobus::NAME tcNAME = ourNAME; + tcNAME.set_function_code(static_cast(isobus::NAME::Function::TaskController)); + + isobus::NAME tecuNAME = ourNAME; + tecuNAME.set_function_code(static_cast(isobus::NAME::Function::TractorECU)); + tecuNAME.set_arbitrary_address_capable(false); // TECU address is fixed + tecuNAME.set_ecu_instance(0); + + std::cout << "[Init] Creating Task Controller control function..." << std::endl; + auto tcCF = isobus::CANNetworkManager::CANNetwork.create_internal_control_function(tcNAME, 0, isobus::preferred_addresses::IndustryGroup2::TaskController_MappingComputer); // The preferred address for a TC is defined in ISO 11783 + auto tcAddressClaimedFuture = std::async(std::launch::async, [&tcCF]() { + while (!tcCF->get_address_valid()) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + isobus::CANNetworkManager::CANNetwork.update(); + } + }); // If this fails, probably the update thread is not started - addressClaimedFuture.wait_for(std::chrono::seconds(5)); - if (!serverCF->get_address_valid()) + tcAddressClaimedFuture.wait_for(std::chrono::seconds(5)); + if (!tcCF->get_address_valid()) { std::cout << "Failed to claim address for TC server. The control function might be invalid." << std::endl; return false; } - tcServer = std::make_shared(serverCF); + // Create TECU control function + // TODO: Should we wait between this and TC? + // TODO: If there's already a TECU on the bus we should not create ours + if (tcCF) + { // Only create TECU if TC was created + std::cout << "[Init] Creating Tractor ECU control function..." << std::endl; + tecuCF = isobus::CANNetworkManager::CANNetwork.create_internal_control_function(tecuNAME, 0, isobus::preferred_addresses::IndustryGroup2::TractorECU); + std::cout << "[Init] Tractor ECU control function created, waiting 1.5 seconds..." << std::endl; + + // Update the network manager to process TECU CF claiming + for (int i = 0; i < 15; i++) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + isobus::CANNetworkManager::CANNetwork.update(); + } + } + + tcServer = std::make_shared(tcCF); auto &languageInterface = tcServer->get_language_command_interface(); languageInterface.set_language_code("en"); // This is the default, but you can change it if you want languageInterface.set_country_code("US"); // This is the default, but you can change it if you want @@ -75,36 +106,44 @@ bool Application::initialize() tcServer->set_task_totals_active(true); // TODO: make this dynamic based on status in AOG // Initialize speed and distance messages - speedMessagesInterface = std::make_unique(serverCF, true, true, true, false); //TODO: make configurable whether to send these messages - speedMessagesInterface->initialize(); - nmea2000MessageInterface = std::make_unique(serverCF, false, false, false, false, false, false, false); - nmea2000MessageInterface->initialize(); - nmea2000MessageInterface->set_enable_sending_cog_sog_cyclically(true); // TODO: make configurable whether to send these messages - - speedMessagesInterface->wheelBasedSpeedTransmitData.set_implement_start_stop_operations_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::ImplementStartStopOperations::NotAvailable); - speedMessagesInterface->wheelBasedSpeedTransmitData.set_key_switch_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::KeySwitchState::NotAvailable); - speedMessagesInterface->wheelBasedSpeedTransmitData.set_operator_direction_reversed_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::OperatorDirectionReversed::NotAvailable); - speedMessagesInterface->machineSelectedSpeedTransmitData.set_speed_source(isobus::SpeedMessagesInterface::MachineSelectedSpeedData::SpeedSource::NavigationBasedSpeed); + if (tecuCF) + { + std::cout << "[Init] Creating Speed Messages Interface on TECU..." << std::endl; + speedMessagesInterface = std::make_unique(tecuCF, true, true, true, false); //TODO: make configurable whether to send these messages + speedMessagesInterface->initialize(); + speedMessagesInterface->wheelBasedSpeedTransmitData.set_implement_start_stop_operations_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::ImplementStartStopOperations::NotAvailable); + speedMessagesInterface->wheelBasedSpeedTransmitData.set_key_switch_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::KeySwitchState::NotAvailable); + speedMessagesInterface->wheelBasedSpeedTransmitData.set_operator_direction_reversed_state(isobus::SpeedMessagesInterface::WheelBasedMachineSpeedData::OperatorDirectionReversed::NotAvailable); + speedMessagesInterface->machineSelectedSpeedTransmitData.set_speed_source(isobus::SpeedMessagesInterface::MachineSelectedSpeedData::SpeedSource::NavigationBasedSpeed); + std::cout << "[Init] Speed Messages Interface created and initialized." << std::endl; + + std::cout << "[Init] Creating NMEA2000 Message Interface on TECU..." << std::endl; + nmea2000MessageInterface = std::make_unique(tecuCF, false, false, false, false, false, false, false); + nmea2000MessageInterface->initialize(); + nmea2000MessageInterface->set_enable_sending_cog_sog_cyclically(true); // TODO: make configurable whether to send these messages + std::cout << "[Init] NMEA2000 Message Interface created and initialized." << std::endl; + } + else + { + std::cout << "[Warning] TECU Control Function not available, Speed/NMEA interfaces not created" << std::endl; + } std::cout << "Task controller server started." << std::endl; static std::uint8_t xteSid = 0; static std::uint32_t lastXteTransmit = 0; - auto packetHandler = [this, serverCF](std::uint8_t src, std::uint8_t pgn, std::span data) { - if (src == 0x7F && pgn == 0xFE) // 254 - Steer Data + auto packetHandler = [this, tcCF](std::uint8_t src, std::uint8_t pgn, std::span data) { + if (src == 0x7F && pgn == 0xE5) // 229 - Section Data 1 to 64 { - // TODO: hack to get desired section states. probably want to make a new pgn later when we need more than 16 sections std::vector sectionStates; - for (std::uint8_t i = 0; i < 8; i++) - { - sectionStates.push_back(data[6] & (1 << i)); - } - for (std::uint8_t i = 0; i < 8; i++) + for (std::uint8_t scb = 0; scb < 8; scb++) { - sectionStates.push_back(data[7] & (1 << i)); + for (std::uint8_t i = 0; i < 8; i++) + { + sectionStates.push_back(data[scb] & (1 << i)); + } } - tcServer->update_section_states(sectionStates); } else if (src == 0x7F && pgn == 0xF1) // 241 - Section Control @@ -122,23 +161,28 @@ bool Application::initialize() { std::uint16_t speed = std::abs(value); auto direction = value < 0 ? isobus::SpeedMessagesInterface::MachineDirection::Reverse : isobus::SpeedMessagesInterface::MachineDirection::Forward; - speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_direction_of_travel(direction); - speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_direction_of_travel(direction); - speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_direction_of_travel(direction); - - speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_speed(speed); - speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_speed(speed); - speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_speed(speed); - - speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance - speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance - speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance - - auto &cog_sog_message = nmea2000MessageInterface->get_cog_sog_transmit_message(); - cog_sog_message.set_sequence_id(nmea2000SequenceIdentifier++); - cog_sog_message.set_speed_over_ground(speed); - cog_sog_message.set_course_over_ground(0); // TODO: Implement course - cog_sog_message.set_course_over_ground_reference(isobus::NMEA2000Messages::CourseOverGroundSpeedOverGroundRapidUpdate::CourseOverGroundReference::NotApplicableOrNull); + if (speedMessagesInterface) + { + speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_direction_of_travel(direction); + speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_direction_of_travel(direction); + speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_direction_of_travel(direction); + + speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_speed(speed); + speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_speed(speed); + speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_speed(speed); + + speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance + speedMessagesInterface->wheelBasedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance + speedMessagesInterface->machineSelectedSpeedTransmitData.set_machine_distance(0); // TODO: Implement distance + } + if (nmea2000MessageInterface) + { + auto &cog_sog_message = nmea2000MessageInterface->get_cog_sog_transmit_message(); + cog_sog_message.set_sequence_id(nmea2000SequenceIdentifier++); + cog_sog_message.set_speed_over_ground(speed / 10); + cog_sog_message.set_course_over_ground(0); // TODO: Implement course + cog_sog_message.set_course_over_ground_reference(isobus::NMEA2000Messages::CourseOverGroundSpeedOverGroundRapidUpdate::CourseOverGroundReference::NotApplicableOrNull); + } } else if (identifier == isobus::DataDescriptionIndex::GuidanceLineDeviation) { @@ -160,13 +204,13 @@ bool Application::initialize() }; if (isobus::SystemTiming::time_expired_ms(lastXteTransmit, 1000)) // Transmit every second { - if (isobus::CANNetworkManager::CANNetwork.send_can_message(0x1F903, xteData.data(), xteData.size(), serverCF)) + if (isobus::CANNetworkManager::CANNetwork.send_can_message(0x1F903, xteData.data(), xteData.size(), tcCF)) { lastXteTransmit = isobus::SystemTiming::get_timestamp_ms(); } } } - else if (static_cast(identifier) == 597 /*isobus::DataDescriptionIndex::TotalDistance*/) + else if (static_cast(identifier) == 597 /*isobus::DataDescriptionIndex::TotalDistance*/ && speedMessagesInterface) { auto distance = static_cast(value); speedMessagesInterface->groundBasedSpeedTransmitData.set_machine_distance(distance); @@ -186,14 +230,17 @@ bool Application::initialize() bool Application::update() { static std::uint32_t lastHeartbeatTransmit = 0; + static std::uint32_t lastTECUStatusTransmit = 0; udpConnections->handle_address_detection(); udpConnections->handle_incoming_packets(); tcServer->request_measurement_commands(); tcServer->update(); - speedMessagesInterface->update(); - nmea2000MessageInterface->update(); + if (speedMessagesInterface) + speedMessagesInterface->update(); + if (nmea2000MessageInterface) + nmea2000MessageInterface->update(); if (isobus::SystemTiming::time_expired_ms(lastHeartbeatTransmit, 100)) { diff --git a/src/main.cpp b/src/main.cpp index 12274a7..6ec6b6c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -116,8 +116,8 @@ class ArgumentProcessor std::cout << "Options:\n"; std::cout << " --help\t\tShow this help message\n"; std::cout << " --version\t\tShow the version of the application\n"; - std::cout << " --adapter=\tSelect the CAN driver\n"; - std::cout << " --channel=\tSelect the CAN channel\n"; + std::cout << " --can_adapter=\tSelect the CAN driver\n"; + std::cout << " --can_channel=\tSelect the CAN channel\n"; std::cout << " --log_level=\tSet the log level (debug, info, warning, error, critical)\n"; std::cout << " --log2file\t\tLog to file\n"; exit(0); diff --git a/src/task_controller.cpp b/src/task_controller.cpp index d776345..5c4a856 100644 --- a/src/task_controller.cpp +++ b/src/task_controller.cpp @@ -135,11 +135,27 @@ void ClientState::set_element_number_for_ddi(isobus::DataDescriptionIndex ddi, s ddiToElementNumber[ddi] = elementNumber; } +void ClientState::set_element_work_state(std::uint16_t elementNumber, bool isWorking) +{ + elementWorkStates[elementNumber] = isWorking; +} + +bool ClientState::try_get_element_work_state(std::uint16_t elementNumber, bool &isWorking) const +{ + auto it = elementWorkStates.find(elementNumber); + if (it != elementWorkStates.end()) + { + isWorking = it->second; + return true; + } + return false; +} + MyTCServer::MyTCServer(std::shared_ptr internalControlFunction) : TaskControllerServer(internalControlFunction, 1, // AOG limits to 1 boom - 16, // AOG limits to 16 sections of unique width - 16, // 16 channels for position based control + 64, // AOG limits to 16 sections of unique width but with zones it can be 64 + 64, // 64 channels for position based control isobus::TaskControllerOptions() .with_implement_section_control(), // We support section control TaskControllerVersion::SecondEditionDraft) @@ -181,7 +197,14 @@ bool MyTCServer::activate_object_pool(std::shared_ptr p break; } } - auto fileName = std::to_string(partnerCF->get_NAME().get_full_name()) + "\\" + std::string(deviceObject->get_localization_label().begin(), deviceObject->get_localization_label().end()) + ".iop"; + + auto labelBytes = deviceObject->get_localization_label(); + std::string label(reinterpret_cast(labelBytes.data()), labelBytes.size()); + // trim at first occurrence of null or ETX (0x03) + auto it = std::find_if(label.begin(), label.end(), [](char c) { return c == '\0' || static_cast(c) == 0x03; }); + label.erase(it, label.end()); + + auto fileName = std::to_string(partnerCF->get_NAME().get_full_name()) + "\\" + label + ".ddop"; std::vector binaryPool; if (state.get_pool().generate_binary_object_pool(binaryPool)) { @@ -190,10 +213,11 @@ bool MyTCServer::activate_object_pool(std::shared_ptr p { outFile.write(reinterpret_cast(binaryPool.data()), binaryPool.size()); outFile.close(); + std::cout << "Saved DDOP to file: " << fileName << std::endl; } else { - std::cout << "Unable to save DDOP to NVM. (Failed to open file)" << std::endl; + std::cout << "Unable to save DDOP to NVM. (Failed to open file) file: " << fileName << std::endl; } } else @@ -327,9 +351,33 @@ bool MyTCServer::on_value_command(std::shared_ptr partn { std::uint8_t sectionIndexOffset = NUMBER_SECTIONS_PER_CONDENSED_MESSAGE * static_cast(dataDescriptionIndex - static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16)); + // Check if ActualWorkState is off (0) for either the current element or element 0 (main implement) + // If either is off, all sections should be treated as off + bool workStateOff = false; + auto &clientState = clients[partner]; + // Check if the current element's work state is off + bool currentElementWorkState; + if (clientState.try_get_element_work_state(elementNumber, currentElementWorkState) && !currentElementWorkState) + { + workStateOff = true; + //std::cout << "Element " << elementNumber << " work state is OFF, forcing sections to OFF" << std::endl; + } + // Check if element 0's work state is off (main implement) + bool mainElementWorkState; + if (clientState.try_get_element_work_state(0, mainElementWorkState)) + { + if (!mainElementWorkState) + { + workStateOff = true; + //std::cout << "Element 0 work state is OFF, forcing sections to OFF" << std::endl; + } + } for (std::uint_fast8_t i = 0; i < NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; i++) { - clients[partner].set_section_actual_state(i + sectionIndexOffset, (processDataValue >> (2 * i)) & 0x03); + // When work state is off, force all sections to off state + // Otherwise, use the actual values from the implement + std::uint8_t sectionState = workStateOff ? SectionState::OFF : ((processDataValue >> (2 * i)) & 0x03); + clients[partner].set_section_actual_state(i + sectionIndexOffset, sectionState); } } break; @@ -342,7 +390,8 @@ bool MyTCServer::on_value_command(std::shared_ptr partn case static_cast(isobus::DataDescriptionIndex::ActualWorkState): { - clients[partner].set_setpoint_work_state(processDataValue == 1); + // Store the work state per element rather than globally + clients[partner].set_element_work_state(elementNumber, processDataValue == 1); } } @@ -394,10 +443,15 @@ void MyTCServer::request_measurement_commands() { // TODO: This is a bit of a hack, but it works for now client.second.set_element_number_for_ddi(static_cast(processDataObject->get_ddi()), elementObject->get_element_number()); + const auto &entryB = isobus::DataDictionary::get_entry(processDataObject->get_ddi()); + std::cout << "Mapped DDI " << processDataObject->get_ddi() << " (" << entryB.to_string() << ") to element " + << elementObject->get_element_number() << std::endl; if (processDataObject->has_trigger_method(isobus::task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::OnChange)) { send_change_threshold_measurement_command(client.first, processDataObject->get_ddi(), elementObject->get_element_number(), 1); + std::cout << "Subscribed (OnChange) to DDI " << processDataObject->get_ddi() << " (" << entryB.to_string() << ") for element " + << elementObject->get_element_number() << std::endl; } if (processDataObject->has_trigger_method(isobus::task_controller_object::DeviceProcessDataObject::AvailableTriggerMethods::TimeInterval)) { @@ -488,7 +542,7 @@ void MyTCServer::update_section_states(std::vector §ionStates) } if (requiresUpdate) { - std::uint8_t ddiOffset = state.get_number_of_sections() / NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; + std::uint8_t ddiOffset = (state.get_number_of_sections() - 1) / NUMBER_SECTIONS_PER_CONDENSED_MESSAGE; send_section_setpoint_states(client.first, ddiOffset); } } @@ -515,16 +569,29 @@ void MyTCServer::send_section_setpoint_states(std::shared_ptr(isobus::DataDescriptionIndex::SetpointCondensedWorkState1_16) + ddiOffset; - std::uint16_t elementNumber = clients[client].get_element_number_for_ddi(static_cast(ddiTarget)); - send_set_value(client, ddiTarget, elementNumber, value); + if (clients[client].get_element_number_for_ddi(static_cast(ddiTarget)) != 0) + { + std::uint16_t elementNumber = clients[client].get_element_number_for_ddi(static_cast(ddiTarget)); + send_set_value(client, ddiTarget, elementNumber, value); - bool setpointWorkState = clients[client].is_any_section_setpoint_on(); - if ((clients[client].get_setpoint_work_state() != setpointWorkState)) + bool setpointWorkState = clients[client].is_any_section_setpoint_on(); + if ((clients[client].get_setpoint_work_state() != setpointWorkState)) + { + send_set_value(client, static_cast(isobus::DataDescriptionIndex::SetpointWorkState), clients[client].get_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState), setpointWorkState ? 1 : 0); + clients[client].set_setpoint_work_state(setpointWorkState); + } + return; + } + ddiTarget = static_cast(isobus::DataDescriptionIndex::ActualCondensedWorkState1_16) + ddiOffset; + if (clients[client].get_element_number_for_ddi(static_cast(ddiTarget)) != 0) { - send_set_value(client, static_cast(isobus::DataDescriptionIndex::SetpointWorkState), clients[client].get_element_number_for_ddi(isobus::DataDescriptionIndex::SetpointWorkState), setpointWorkState ? 1 : 0); - clients[client].set_setpoint_work_state(setpointWorkState); + send_set_value(client, ddiTarget, clients[client].get_element_number_for_ddi(static_cast(ddiTarget)), value); + return; } + + std::cout << "[TC Server] Neither condensed nor controllable-actual work state supported Missing DDI 290 and 141!" << std::endl; } void MyTCServer::send_section_control_state(std::shared_ptr client, bool enabled)