mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Add E1.31 support (#950)
This adds a `e131` component that allows to register `e131` addressable light effect. This uses an internal implementation that is thread-safe instead of using external libraries.
This commit is contained in:
		
							
								
								
									
										49
									
								
								esphome/components/e131/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								esphome/components/e131/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components.light.types import AddressableLightEffect | ||||
| from esphome.components.light.effects import register_addressable_effect | ||||
| from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS | ||||
|  | ||||
| e131_ns = cg.esphome_ns.namespace('e131') | ||||
| E131AddressableLightEffect = e131_ns.class_('E131AddressableLightEffect', AddressableLightEffect) | ||||
| E131Component = e131_ns.class_('E131Component', cg.Component) | ||||
|  | ||||
| METHODS = { | ||||
|     'UNICAST': e131_ns.E131_UNICAST, | ||||
|     'MULTICAST': e131_ns.E131_MULTICAST | ||||
| } | ||||
|  | ||||
| CHANNELS = { | ||||
|     'MONO': e131_ns.E131_MONO, | ||||
|     'RGB': e131_ns.E131_RGB, | ||||
|     'RGBW': e131_ns.E131_RGBW | ||||
| } | ||||
|  | ||||
| CONF_UNIVERSE = 'universe' | ||||
| CONF_E131_ID = 'e131_id' | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema({ | ||||
|     cv.GenerateID(): cv.declare_id(E131Component), | ||||
|     cv.Optional(CONF_METHOD, default='MULTICAST'): cv.one_of(*METHODS, upper=True), | ||||
| }) | ||||
|  | ||||
|  | ||||
| def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     yield cg.register_component(var, config) | ||||
|     cg.add(var.set_method(METHODS[config[CONF_METHOD]])) | ||||
|  | ||||
|  | ||||
| @register_addressable_effect('e131', E131AddressableLightEffect, "E1.31", { | ||||
|     cv.GenerateID(CONF_E131_ID): cv.use_id(E131Component), | ||||
|     cv.Required(CONF_UNIVERSE): cv.int_range(min=1, max=512), | ||||
|     cv.Optional(CONF_CHANNELS, default='RGB'): cv.one_of(*CHANNELS, upper=True) | ||||
| }) | ||||
| def e131_light_effect_to_code(config, effect_id): | ||||
|     parent = yield cg.get_variable(config[CONF_E131_ID]) | ||||
|  | ||||
|     effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) | ||||
|     cg.add(effect.set_first_universe(config[CONF_UNIVERSE])) | ||||
|     cg.add(effect.set_channels(CHANNELS[config[CONF_CHANNELS]])) | ||||
|     cg.add(effect.set_e131(parent)) | ||||
|     yield effect | ||||
							
								
								
									
										105
									
								
								esphome/components/e131/e131.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								esphome/components/e131/e131.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| #include "e131.h" | ||||
| #include "e131_addressable_light_effect.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
| #include <WiFi.h> | ||||
| #endif | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP8266 | ||||
| #include <ESP8266WiFi.h> | ||||
| #include <WiFiUdp.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace e131 { | ||||
|  | ||||
| static const char *TAG = "e131"; | ||||
| static const int PORT = 5568; | ||||
|  | ||||
| E131Component::E131Component() {} | ||||
|  | ||||
| E131Component::~E131Component() { | ||||
|   if (udp_) { | ||||
|     udp_->stop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void E131Component::setup() { | ||||
|   udp_.reset(new WiFiUDP()); | ||||
|  | ||||
|   if (!udp_->begin(PORT)) { | ||||
|     ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); | ||||
|     mark_failed(); | ||||
|   } | ||||
|  | ||||
|   join_igmp_groups_(); | ||||
| } | ||||
|  | ||||
| void E131Component::loop() { | ||||
|   std::vector<uint8_t> payload; | ||||
|   E131Packet packet; | ||||
|   int universe = 0; | ||||
|  | ||||
|   while (uint16_t packet_size = udp_->parsePacket()) { | ||||
|     payload.resize(packet_size); | ||||
|  | ||||
|     if (!udp_->read(&payload[0], payload.size())) { | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     if (!packet_(payload, universe, packet)) { | ||||
|       ESP_LOGV(TAG, "Invalid packet recevied of size %zu.", payload.size()); | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     if (!process_(universe, packet)) { | ||||
|       ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void E131Component::add_effect(E131AddressableLightEffect *light_effect) { | ||||
|   if (light_effects_.count(light_effect)) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), | ||||
|            light_effect->get_first_universe(), light_effect->get_last_universe()); | ||||
|  | ||||
|   light_effects_.insert(light_effect); | ||||
|  | ||||
|   for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { | ||||
|     join_(universe); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { | ||||
|   if (!light_effects_.count(light_effect)) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), | ||||
|            light_effect->get_first_universe(), light_effect->get_last_universe()); | ||||
|  | ||||
|   light_effects_.erase(light_effect); | ||||
|  | ||||
|   for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { | ||||
|     leave_(universe); | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool E131Component::process_(int universe, const E131Packet &packet) { | ||||
|   bool handled = false; | ||||
|  | ||||
|   ESP_LOGV(TAG, "Received E1.31 packet for %d universe, with %d bytes", universe, packet.count); | ||||
|  | ||||
|   for (auto light_effect : light_effects_) { | ||||
|     handled = light_effect->process_(universe, packet) || handled; | ||||
|   } | ||||
|  | ||||
|   return handled; | ||||
| } | ||||
|  | ||||
| }  // namespace e131 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										57
									
								
								esphome/components/e131/e131.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								esphome/components/e131/e131.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| #include <memory> | ||||
| #include <set> | ||||
| #include <map> | ||||
|  | ||||
| class UDP; | ||||
|  | ||||
| namespace esphome { | ||||
| namespace e131 { | ||||
|  | ||||
| class E131AddressableLightEffect; | ||||
|  | ||||
| enum E131ListenMethod { E131_MULTICAST, E131_UNICAST }; | ||||
|  | ||||
| const int E131_MAX_PROPERTY_VALUES_COUNT = 513; | ||||
|  | ||||
| struct E131Packet { | ||||
|   uint16_t count; | ||||
|   uint8_t values[E131_MAX_PROPERTY_VALUES_COUNT]; | ||||
| }; | ||||
|  | ||||
| class E131Component : public esphome::Component { | ||||
|  public: | ||||
|   E131Component(); | ||||
|   ~E131Component(); | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } | ||||
|  | ||||
|  public: | ||||
|   void add_effect(E131AddressableLightEffect *light_effect); | ||||
|   void remove_effect(E131AddressableLightEffect *light_effect); | ||||
|  | ||||
|  public: | ||||
|   void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } | ||||
|  | ||||
|  protected: | ||||
|   bool packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet); | ||||
|   bool process_(int universe, const E131Packet &packet); | ||||
|   bool join_igmp_groups_(); | ||||
|   void join_(int universe); | ||||
|   void leave_(int universe); | ||||
|  | ||||
|  protected: | ||||
|   E131ListenMethod listen_method_{E131_MULTICAST}; | ||||
|   std::unique_ptr<UDP> udp_; | ||||
|   std::set<E131AddressableLightEffect *> light_effects_; | ||||
|   std::map<int, int> universe_consumers_; | ||||
|   std::map<int, E131Packet> universe_packets_; | ||||
| }; | ||||
|  | ||||
| }  // namespace e131 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										90
									
								
								esphome/components/e131/e131_addressable_light_effect.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								esphome/components/e131/e131_addressable_light_effect.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| #include "e131.h" | ||||
| #include "e131_addressable_light_effect.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace e131 { | ||||
|  | ||||
| static const char *TAG = "e131_addressable_light_effect"; | ||||
| static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); | ||||
|  | ||||
| E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|  | ||||
| int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } | ||||
|  | ||||
| int E131AddressableLightEffect::get_lights_per_universe() const { return MAX_DATA_SIZE / channels_; } | ||||
|  | ||||
| int E131AddressableLightEffect::get_first_universe() const { return first_universe_; } | ||||
|  | ||||
| int E131AddressableLightEffect::get_last_universe() const { return first_universe_ + get_universe_count() - 1; } | ||||
|  | ||||
| int E131AddressableLightEffect::get_universe_count() const { | ||||
|   // Round up to lights_per_universe | ||||
|   auto lights = get_lights_per_universe(); | ||||
|   return (get_addressable_()->size() + lights - 1) / lights; | ||||
| } | ||||
|  | ||||
| void E131AddressableLightEffect::start() { | ||||
|   AddressableLightEffect::start(); | ||||
|  | ||||
|   if (this->e131_) { | ||||
|     this->e131_->add_effect(this); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void E131AddressableLightEffect::stop() { | ||||
|   if (this->e131_) { | ||||
|     this->e131_->remove_effect(this); | ||||
|   } | ||||
|  | ||||
|   AddressableLightEffect::stop(); | ||||
| } | ||||
|  | ||||
| void E131AddressableLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { | ||||
|   // ignore, it is run by `E131Component::update()` | ||||
| } | ||||
|  | ||||
| bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet) { | ||||
|   auto it = get_addressable_(); | ||||
|  | ||||
|   // check if this is our universe and data are valid | ||||
|   if (universe < first_universe_ || universe > get_last_universe()) | ||||
|     return false; | ||||
|  | ||||
|   int output_offset = (universe - first_universe_) * get_lights_per_universe(); | ||||
|   // limit amount of lights per universe and received | ||||
|   int output_end = std::min(it->size(), std::min(output_offset + get_lights_per_universe(), packet.count - 1)); | ||||
|   auto input_data = packet.values + 1; | ||||
|  | ||||
|   ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %d-%d.", get_name().c_str(), universe, output_offset, | ||||
|            output_end); | ||||
|  | ||||
|   switch (channels_) { | ||||
|     case E131_MONO: | ||||
|       for (; output_offset < output_end; output_offset++, input_data++) { | ||||
|         auto output = (*it)[output_offset]; | ||||
|         output.set(light::ESPColor(input_data[0], input_data[0], input_data[0], input_data[0])); | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case E131_RGB: | ||||
|       for (; output_offset < output_end; output_offset++, input_data += 3) { | ||||
|         auto output = (*it)[output_offset]; | ||||
|         output.set(light::ESPColor(input_data[0], input_data[1], input_data[2], | ||||
|                                    (input_data[0] + input_data[1] + input_data[2]) / 3)); | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case E131_RGBW: | ||||
|       for (; output_offset < output_end; output_offset++, input_data += 4) { | ||||
|         auto output = (*it)[output_offset]; | ||||
|         output.set(light::ESPColor(input_data[0], input_data[1], input_data[2], input_data[3])); | ||||
|       } | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| }  // namespace e131 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										48
									
								
								esphome/components/e131/e131_addressable_light_effect.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								esphome/components/e131/e131_addressable_light_effect.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/light/addressable_light_effect.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace e131 { | ||||
|  | ||||
| class E131Component; | ||||
| struct E131Packet; | ||||
|  | ||||
| enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; | ||||
|  | ||||
| class E131AddressableLightEffect : public light::AddressableLightEffect { | ||||
|  public: | ||||
|   E131AddressableLightEffect(const std::string &name); | ||||
|  | ||||
|  public: | ||||
|   void start() override; | ||||
|   void stop() override; | ||||
|   void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; | ||||
|  | ||||
|  public: | ||||
|   int get_data_per_universe() const; | ||||
|   int get_lights_per_universe() const; | ||||
|   int get_first_universe() const; | ||||
|   int get_last_universe() const; | ||||
|   int get_universe_count() const; | ||||
|  | ||||
|  public: | ||||
|   void set_first_universe(int universe) { this->first_universe_ = universe; } | ||||
|   void set_channels(E131LightChannels channels) { this->channels_ = channels; } | ||||
|   void set_e131(E131Component *e131) { this->e131_ = e131; } | ||||
|  | ||||
|  protected: | ||||
|   bool process_(int universe, const E131Packet &packet); | ||||
|  | ||||
|  protected: | ||||
|   int first_universe_{0}; | ||||
|   int last_universe_{0}; | ||||
|   E131LightChannels channels_{E131_RGB}; | ||||
|   E131Component *e131_{nullptr}; | ||||
|  | ||||
|   friend class E131Component; | ||||
| }; | ||||
|  | ||||
| }  // namespace e131 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										136
									
								
								esphome/components/e131/e131_packet.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								esphome/components/e131/e131_packet.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| #include "e131.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/util.h" | ||||
|  | ||||
| #include <lwip/ip_addr.h> | ||||
| #include <lwip/igmp.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace e131 { | ||||
|  | ||||
| static const char *TAG = "e131"; | ||||
|  | ||||
| static const uint8_t ACN_ID[12] = {0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00}; | ||||
| static const uint32_t VECTOR_ROOT = 4; | ||||
| static const uint32_t VECTOR_FRAME = 2; | ||||
| static const uint8_t VECTOR_DMP = 2; | ||||
|  | ||||
| // E1.31 Packet Structure | ||||
| union E131RawPacket { | ||||
|   struct { | ||||
|     // Root Layer | ||||
|     uint16_t preamble_size; | ||||
|     uint16_t postamble_size; | ||||
|     uint8_t acn_id[12]; | ||||
|     uint16_t root_flength; | ||||
|     uint32_t root_vector; | ||||
|     uint8_t cid[16]; | ||||
|  | ||||
|     // Frame Layer | ||||
|     uint16_t frame_flength; | ||||
|     uint32_t frame_vector; | ||||
|     uint8_t source_name[64]; | ||||
|     uint8_t priority; | ||||
|     uint16_t reserved; | ||||
|     uint8_t sequence_number; | ||||
|     uint8_t options; | ||||
|     uint16_t universe; | ||||
|  | ||||
|     // DMP Layer | ||||
|     uint16_t dmp_flength; | ||||
|     uint8_t dmp_vector; | ||||
|     uint8_t type; | ||||
|     uint16_t first_address; | ||||
|     uint16_t address_increment; | ||||
|     uint16_t property_value_count; | ||||
|     uint8_t property_values[E131_MAX_PROPERTY_VALUES_COUNT]; | ||||
|   } __attribute__((packed)); | ||||
|  | ||||
|   uint8_t raw[638]; | ||||
| }; | ||||
|  | ||||
| // We need to have at least one `1` value | ||||
| // Get the offset of `property_values[1]` | ||||
| const long E131_MIN_PACKET_SIZE = reinterpret_cast<long>(&((E131RawPacket *) nullptr)->property_values[1]); | ||||
|  | ||||
| bool E131Component::join_igmp_groups_() { | ||||
|   if (listen_method_ != E131_MULTICAST) | ||||
|     return false; | ||||
|   if (!udp_) | ||||
|     return false; | ||||
|  | ||||
|   for (auto universe : universe_consumers_) { | ||||
|     if (!universe.second) | ||||
|       continue; | ||||
|  | ||||
|     ip4_addr_t multicast_addr = { | ||||
|         static_cast<uint32_t>(IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)))}; | ||||
|  | ||||
|     auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); | ||||
|  | ||||
|     if (err) { | ||||
|       ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void E131Component::join_(int universe) { | ||||
|   // store only latest received packet for the given universe | ||||
|   auto consumers = ++universe_consumers_[universe]; | ||||
|  | ||||
|   if (consumers > 1) { | ||||
|     return;  // we already joined before | ||||
|   } | ||||
|  | ||||
|   if (join_igmp_groups_()) { | ||||
|     ESP_LOGD(TAG, "Joined %d universe for E1.31.", universe); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void E131Component::leave_(int universe) { | ||||
|   auto consumers = --universe_consumers_[universe]; | ||||
|  | ||||
|   if (consumers > 0) { | ||||
|     return;  // we have other consumers of the given universe | ||||
|   } | ||||
|  | ||||
|   if (listen_method_ == E131_MULTICAST) { | ||||
|     ip4_addr_t multicast_addr = { | ||||
|         static_cast<uint32_t>(IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)))}; | ||||
|  | ||||
|     igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Left %d universe for E1.31.", universe); | ||||
| } | ||||
|  | ||||
| bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E131Packet &packet) { | ||||
|   if (data.size() < E131_MIN_PACKET_SIZE) | ||||
|     return false; | ||||
|  | ||||
|   auto sbuff = reinterpret_cast<const E131RawPacket *>(&data[0]); | ||||
|  | ||||
|   if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0) | ||||
|     return false; | ||||
|   if (htonl(sbuff->root_vector) != VECTOR_ROOT) | ||||
|     return false; | ||||
|   if (htonl(sbuff->frame_vector) != VECTOR_FRAME) | ||||
|     return false; | ||||
|   if (sbuff->dmp_vector != VECTOR_DMP) | ||||
|     return false; | ||||
|   if (sbuff->property_values[0] != 0) | ||||
|     return false; | ||||
|  | ||||
|   universe = htons(sbuff->universe); | ||||
|   packet.count = htons(sbuff->property_value_count); | ||||
|   if (packet.count > E131_MAX_PROPERTY_VALUES_COUNT) | ||||
|     return false; | ||||
|  | ||||
|   memcpy(packet.values, sbuff->property_values, packet.count); | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| }  // namespace e131 | ||||
| }  // namespace esphome | ||||
| @@ -1048,6 +1048,8 @@ output: | ||||
|     pin: GPIO25 | ||||
|     id: dac_output | ||||
|  | ||||
| e131: | ||||
|  | ||||
| light: | ||||
|   - platform: binary | ||||
|     name: "Desk Lamp" | ||||
| @@ -1189,6 +1191,8 @@ light: | ||||
|               red: 0% | ||||
|               green: 100% | ||||
|               blue: 0% | ||||
|     - e131: | ||||
|         universe: 1 | ||||
|   - platform: fastled_spi | ||||
|     id: addr2 | ||||
|     chipset: WS2801 | ||||
|   | ||||
| @@ -697,6 +697,8 @@ mcp23017: | ||||
| mcp23008: | ||||
|   id: mcp23008_hub | ||||
|  | ||||
| e131: | ||||
|  | ||||
| light: | ||||
|   - platform: neopixelbus | ||||
|     name: Neopixelbus Light | ||||
| @@ -705,6 +707,9 @@ light: | ||||
|     variant: SK6812 | ||||
|     method: ESP8266_UART0 | ||||
|     num_leds: 100 | ||||
|     effects: | ||||
|       - e131: | ||||
|           universe: 1 | ||||
|  | ||||
| servo: | ||||
|   id: my_servo | ||||
|   | ||||
		Reference in New Issue
	
	Block a user