mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'fixed_ble_adv' into integration
This commit is contained in:
		| @@ -1438,7 +1438,7 @@ message BluetoothLERawAdvertisementsResponse { | |||||||
|   option (ifdef) = "USE_BLUETOOTH_PROXY"; |   option (ifdef) = "USE_BLUETOOTH_PROXY"; | ||||||
|   option (no_delay) = true; |   option (no_delay) = true; | ||||||
|  |  | ||||||
|   repeated BluetoothLERawAdvertisement advertisements = 1; |   repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"]; | ||||||
| } | } | ||||||
|  |  | ||||||
| enum BluetoothDeviceRequestType { | enum BluetoothDeviceRequestType { | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ extend google.protobuf.FieldOptions { | |||||||
|     optional bool no_zero_copy = 50008 [default=false]; |     optional bool no_zero_copy = 50008 [default=false]; | ||||||
|     optional bool fixed_array_skip_zero = 50009 [default=false]; |     optional bool fixed_array_skip_zero = 50009 [default=false]; | ||||||
|     optional string fixed_array_size_define = 50010; |     optional string fixed_array_size_define = 50010; | ||||||
|  |     optional string fixed_array_with_length_define = 50011; | ||||||
|  |  | ||||||
|     // container_pointer: Zero-copy optimization for repeated fields. |     // container_pointer: Zero-copy optimization for repeated fields. | ||||||
|     // |     // | ||||||
|   | |||||||
| @@ -1843,12 +1843,14 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const { | |||||||
|   size.add_length(1, this->data_len); |   size.add_length(1, this->data_len); | ||||||
| } | } | ||||||
| void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { | void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { | ||||||
|   for (auto &it : this->advertisements) { |   for (uint16_t i = 0; i < this->advertisements_len; i++) { | ||||||
|     buffer.encode_message(1, it, true); |     buffer.encode_message(1, this->advertisements[i], true); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { | void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { | ||||||
|   size.add_repeated_message(1, this->advertisements); |   for (uint16_t i = 0; i < this->advertisements_len; i++) { | ||||||
|  |     size.add_message_object_force(1, this->advertisements[i]); | ||||||
|  |   } | ||||||
| } | } | ||||||
| bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|   switch (field_id) { |   switch (field_id) { | ||||||
|   | |||||||
| @@ -1788,11 +1788,12 @@ class BluetoothLERawAdvertisement : public ProtoMessage { | |||||||
| class BluetoothLERawAdvertisementsResponse : public ProtoMessage { | class BluetoothLERawAdvertisementsResponse : public ProtoMessage { | ||||||
|  public: |  public: | ||||||
|   static constexpr uint8_t MESSAGE_TYPE = 93; |   static constexpr uint8_t MESSAGE_TYPE = 93; | ||||||
|   static constexpr uint8_t ESTIMATED_SIZE = 34; |   static constexpr uint8_t ESTIMATED_SIZE = 136; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } |   const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; } | ||||||
| #endif | #endif | ||||||
|   std::vector<BluetoothLERawAdvertisement> advertisements{}; |   std::array<BluetoothLERawAdvertisement, BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE> advertisements{}; | ||||||
|  |   uint16_t advertisements_len{0}; | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
|   void calculate_size(ProtoSize &size) const override; |   void calculate_size(ProtoSize &size) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   | |||||||
| @@ -1534,9 +1534,9 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const { | |||||||
| } | } | ||||||
| void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { | void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { | ||||||
|   MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); |   MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); | ||||||
|   for (const auto &it : this->advertisements) { |   for (uint16_t i = 0; i < this->advertisements_len; i++) { | ||||||
|     out.append("  advertisements: "); |     out.append("  advertisements: "); | ||||||
|     it.dump_to(out); |     this->advertisements[i].dump_to(out); | ||||||
|     out.append("\n"); |     out.append("\n"); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -118,6 +118,12 @@ async def to_code(config): | |||||||
|     connection_count = len(config.get(CONF_CONNECTIONS, [])) |     connection_count = len(config.get(CONF_CONNECTIONS, [])) | ||||||
|     cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count) |     cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count) | ||||||
|  |  | ||||||
|  |     # Define batch size for BLE advertisements | ||||||
|  |     # Each advertisement is up to 80 bytes when packaged (including protocol overhead) | ||||||
|  |     # 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload | ||||||
|  |     # This achieves ~97% WiFi MTU utilization while staying under the limit | ||||||
|  |     cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) | ||||||
|  |  | ||||||
|     for connection_conf in config.get(CONF_CONNECTIONS, []): |     for connection_conf in config.get(CONF_CONNECTIONS, []): | ||||||
|         connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) |         connection_var = cg.new_Pvariable(connection_conf[CONF_ID]) | ||||||
|         await cg.register_component(connection_var, connection_conf) |         await cg.register_component(connection_var, connection_conf) | ||||||
|   | |||||||
| @@ -11,12 +11,8 @@ namespace esphome::bluetooth_proxy { | |||||||
|  |  | ||||||
| static const char *const TAG = "bluetooth_proxy"; | static const char *const TAG = "bluetooth_proxy"; | ||||||
|  |  | ||||||
| // Batch size for BLE advertisements to maximize WiFi efficiency | // BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE is defined during code generation | ||||||
| // Each advertisement is up to 80 bytes when packaged (including protocol overhead) | // It sets the batch size for BLE advertisements to maximize WiFi efficiency | ||||||
| // Most advertisements are 20-30 bytes, allowing even more to fit per packet |  | ||||||
| // 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload |  | ||||||
| // This achieves ~97% WiFi MTU utilization while staying under the limit |  | ||||||
| static constexpr size_t FLUSH_BATCH_SIZE = 16; |  | ||||||
|  |  | ||||||
| // Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response) | // Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response) | ||||||
| static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62, | static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62, | ||||||
| @@ -25,13 +21,6 @@ static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62 | |||||||
| BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } | BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } | ||||||
|  |  | ||||||
| void BluetoothProxy::setup() { | void BluetoothProxy::setup() { | ||||||
|   // Reserve capacity but start with size 0 |  | ||||||
|   // Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE |  | ||||||
|   this->response_.advertisements.reserve(FLUSH_BATCH_SIZE / 2); |  | ||||||
|  |  | ||||||
|   // Don't pre-allocate pool - let it grow only if needed in busy environments |  | ||||||
|   // Many devices in quiet areas will never need the overflow pool |  | ||||||
|  |  | ||||||
|   this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS; |   this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS; | ||||||
|   this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS; |   this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS; | ||||||
|  |  | ||||||
| @@ -82,68 +71,45 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, | |||||||
|   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) |   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) | ||||||
|     return false; |     return false; | ||||||
|  |  | ||||||
|   auto &advertisements = this->response_.advertisements; |  | ||||||
|  |  | ||||||
|   for (size_t i = 0; i < count; i++) { |   for (size_t i = 0; i < count; i++) { | ||||||
|     auto &result = scan_results[i]; |     auto &result = scan_results[i]; | ||||||
|     uint8_t length = result.adv_data_len + result.scan_rsp_len; |     uint8_t length = result.adv_data_len + result.scan_rsp_len; | ||||||
|  |  | ||||||
|     // Check if we need to expand the vector |     // Check if we're at capacity | ||||||
|     if (this->advertisement_count_ >= advertisements.size()) { |     if (this->response_.advertisements_len >= BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE) { | ||||||
|       if (this->advertisement_pool_.empty()) { |       // Flush the batch before adding more | ||||||
|         // No room in pool, need to allocate |       this->flush_pending_advertisements(); | ||||||
|         advertisements.emplace_back(); |  | ||||||
|       } else { |  | ||||||
|         // Pull from pool |  | ||||||
|         advertisements.push_back(std::move(this->advertisement_pool_.back())); |  | ||||||
|         this->advertisement_pool_.pop_back(); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Fill in the data directly at current position |     // Fill in the data directly at current position | ||||||
|     auto &adv = advertisements[this->advertisement_count_]; |     auto &adv = this->response_.advertisements[this->response_.advertisements_len]; | ||||||
|     adv.address = esp32_ble::ble_addr_to_uint64(result.bda); |     adv.address = esp32_ble::ble_addr_to_uint64(result.bda); | ||||||
|     adv.rssi = result.rssi; |     adv.rssi = result.rssi; | ||||||
|     adv.address_type = result.ble_addr_type; |     adv.address_type = result.ble_addr_type; | ||||||
|     adv.data_len = length; |     adv.data_len = length; | ||||||
|     std::memcpy(adv.data, result.ble_adv, length); |     std::memcpy(adv.data, result.ble_adv, length); | ||||||
|  |  | ||||||
|     this->advertisement_count_++; |     this->response_.advertisements_len++; | ||||||
|  |  | ||||||
|     ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], |     ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0], | ||||||
|              result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); |              result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); | ||||||
|  |  | ||||||
|     // Flush if we have reached FLUSH_BATCH_SIZE |  | ||||||
|     if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) { |  | ||||||
|       this->flush_pending_advertisements(); |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::flush_pending_advertisements() { | void BluetoothProxy::flush_pending_advertisements() { | ||||||
|   if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr) |   if (this->response_.advertisements_len == 0 || !api::global_api_server->is_connected() || | ||||||
|  |       this->api_connection_ == nullptr) | ||||||
|     return; |     return; | ||||||
|  |  | ||||||
|   auto &advertisements = this->response_.advertisements; |  | ||||||
|  |  | ||||||
|   // Return any items beyond advertisement_count_ to the pool |  | ||||||
|   if (advertisements.size() > this->advertisement_count_) { |  | ||||||
|     // Move unused items back to pool |  | ||||||
|     this->advertisement_pool_.insert(this->advertisement_pool_.end(), |  | ||||||
|                                      std::make_move_iterator(advertisements.begin() + this->advertisement_count_), |  | ||||||
|                                      std::make_move_iterator(advertisements.end())); |  | ||||||
|  |  | ||||||
|     // Resize to actual count |  | ||||||
|     advertisements.resize(this->advertisement_count_); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Send the message |   // Send the message | ||||||
|   this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); |   this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE); | ||||||
|  |  | ||||||
|   // Reset count - existing items will be overwritten in next batch |   ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len); | ||||||
|   this->advertisement_count_ = 0; |  | ||||||
|  |   // Reset the length for the next batch | ||||||
|  |   this->response_.advertisements_len = 0; | ||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::dump_config() { | void BluetoothProxy::dump_config() { | ||||||
|   | |||||||
| @@ -150,7 +150,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | |||||||
|   std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{}; |   std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{}; | ||||||
|  |  | ||||||
|   // BLE advertisement batching |   // BLE advertisement batching | ||||||
|   std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_; |  | ||||||
|   api::BluetoothLERawAdvertisementsResponse response_; |   api::BluetoothLERawAdvertisementsResponse response_; | ||||||
|  |  | ||||||
|   // Group 3: 4-byte types |   // Group 3: 4-byte types | ||||||
| @@ -161,9 +160,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | |||||||
|  |  | ||||||
|   // Group 4: 1-byte types grouped together |   // Group 4: 1-byte types grouped together | ||||||
|   bool active_; |   bool active_; | ||||||
|   uint8_t advertisement_count_{0}; |  | ||||||
|   uint8_t connection_count_{0}; |   uint8_t connection_count_{0}; | ||||||
|   // 3 bytes used, 1 byte padding |   // 2 bytes used, 2 bytes padding | ||||||
| }; | }; | ||||||
|  |  | ||||||
| extern BluetoothProxy *global_bluetooth_proxy;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | extern BluetoothProxy *global_bluetooth_proxy;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|   | |||||||
| @@ -48,6 +48,15 @@ CONFIG_SCHEMA = cv.All( | |||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     if CORE.using_zephyr: |     if CORE.using_zephyr: | ||||||
|         zephyr_add_prj_conf("HWINFO", True) |         zephyr_add_prj_conf("HWINFO", True) | ||||||
|  |         # gdb thread support | ||||||
|  |         zephyr_add_prj_conf("DEBUG_THREAD_INFO", True) | ||||||
|  |         # RTT | ||||||
|  |         zephyr_add_prj_conf("USE_SEGGER_RTT", True) | ||||||
|  |         zephyr_add_prj_conf("RTT_CONSOLE", True) | ||||||
|  |         zephyr_add_prj_conf("LOG", True) | ||||||
|  |         zephyr_add_prj_conf("LOG_BLOCK_IN_THREAD", True) | ||||||
|  |         zephyr_add_prj_conf("LOG_BUFFER_SIZE", 4096) | ||||||
|  |         zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True) | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ from esphome.const import ( | |||||||
|     CONF_ROTATION, |     CONF_ROTATION, | ||||||
|     CONF_TO, |     CONF_TO, | ||||||
|     CONF_TRIGGER_ID, |     CONF_TRIGGER_ID, | ||||||
|  |     CONF_UPDATE_INTERVAL, | ||||||
|  |     SCHEDULER_DONT_RUN, | ||||||
| ) | ) | ||||||
| from esphome.core import coroutine_with_priority | from esphome.core import coroutine_with_priority | ||||||
|  |  | ||||||
| @@ -67,6 +69,18 @@ BASIC_DISPLAY_SCHEMA = cv.Schema( | |||||||
|     } |     } | ||||||
| ).extend(cv.polling_component_schema("1s")) | ).extend(cv.polling_component_schema("1s")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _validate_test_card(config): | ||||||
|  |     if ( | ||||||
|  |         config.get(CONF_SHOW_TEST_CARD, False) | ||||||
|  |         and config.get(CONF_UPDATE_INTERVAL, False) == SCHEDULER_DONT_RUN | ||||||
|  |     ): | ||||||
|  |         raise cv.Invalid( | ||||||
|  |             f"`{CONF_SHOW_TEST_CARD}: True` cannot be used with `{CONF_UPDATE_INTERVAL}: never` because this combination will not show a test_card." | ||||||
|  |         ) | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( | FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( | ||||||
|     { |     { | ||||||
|         cv.Optional(CONF_ROTATION): validate_rotation, |         cv.Optional(CONF_ROTATION): validate_rotation, | ||||||
| @@ -94,6 +108,7 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend( | |||||||
|         cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, |         cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean, | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  | FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def setup_display_core_(var, config): | async def setup_display_core_(var, config): | ||||||
| @@ -200,7 +215,6 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg, | |||||||
|     page = await cg.get_variable(config[CONF_PAGE_ID]) |     page = await cg.get_variable(config[CONF_PAGE_ID]) | ||||||
|     var = cg.new_Pvariable(condition_id, template_arg, paren) |     var = cg.new_Pvariable(condition_id, template_arg, paren) | ||||||
|     cg.add(var.set_page(page)) |     cg.add(var.set_page(page)) | ||||||
|  |  | ||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1331,3 +1331,7 @@ ENTITY_CATEGORY_CONFIG = "config" | |||||||
|  |  | ||||||
| # The entity category for read only diagnostic values, for example RSSI, uptime or MAC Address | # The entity category for read only diagnostic values, for example RSSI, uptime or MAC Address | ||||||
| ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic" | ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic" | ||||||
|  |  | ||||||
|  | # The corresponding constant exists in c++ | ||||||
|  | # when update_interval is set to never, it becomes SCHEDULER_DONT_RUN milliseconds | ||||||
|  | SCHEDULER_DONT_RUN = 4294967295 | ||||||
|   | |||||||
| @@ -148,6 +148,7 @@ | |||||||
|  |  | ||||||
| #define USE_BLUETOOTH_PROXY | #define USE_BLUETOOTH_PROXY | ||||||
| #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 | #define BLUETOOTH_PROXY_MAX_CONNECTIONS 3 | ||||||
|  | #define BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE 16 | ||||||
| #define USE_CAPTIVE_PORTAL | #define USE_CAPTIVE_PORTAL | ||||||
| #define USE_ESP32_BLE | #define USE_ESP32_BLE | ||||||
| #define USE_ESP32_BLE_CLIENT | #define USE_ESP32_BLE_CLIENT | ||||||
|   | |||||||
| @@ -339,6 +339,11 @@ def create_field_type_info( | |||||||
| ) -> TypeInfo: | ) -> TypeInfo: | ||||||
|     """Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options.""" |     """Create the appropriate TypeInfo instance for a field, handling repeated fields and custom options.""" | ||||||
|     if field.label == 3:  # repeated |     if field.label == 3:  # repeated | ||||||
|  |         # Check if this repeated field has fixed_array_with_length_define option | ||||||
|  |         if ( | ||||||
|  |             fixed_size := get_field_opt(field, pb.fixed_array_with_length_define) | ||||||
|  |         ) is not None: | ||||||
|  |             return FixedArrayWithLengthRepeatedType(field, fixed_size) | ||||||
|         # Check if this repeated field has fixed_array_size option |         # Check if this repeated field has fixed_array_size option | ||||||
|         if (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None: |         if (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None: | ||||||
|             return FixedArrayRepeatedType(field, fixed_size) |             return FixedArrayRepeatedType(field, fixed_size) | ||||||
| @@ -1084,6 +1089,12 @@ class FixedArrayRepeatedType(TypeInfo): | |||||||
|         validate_field_type(field.type, field.name) |         validate_field_type(field.type, field.name) | ||||||
|         self._ti: TypeInfo = TYPE_INFO[field.type](field) |         self._ti: TypeInfo = TYPE_INFO[field.type](field) | ||||||
|  |  | ||||||
|  |     def _encode_element(self, element: str) -> str: | ||||||
|  |         """Helper to generate encode statement for a single element.""" | ||||||
|  |         if isinstance(self._ti, EnumType): | ||||||
|  |             return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);" | ||||||
|  |         return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" | ||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def cpp_type(self) -> str: |     def cpp_type(self) -> str: | ||||||
|         return f"std::array<{self._ti.cpp_type}, {self.array_size}>" |         return f"std::array<{self._ti.cpp_type}, {self.array_size}>" | ||||||
| @@ -1111,19 +1122,13 @@ class FixedArrayRepeatedType(TypeInfo): | |||||||
|  |  | ||||||
|     @property |     @property | ||||||
|     def encode_content(self) -> str: |     def encode_content(self) -> str: | ||||||
|         # Helper to generate encode statement for a single element |  | ||||||
|         def encode_element(element: str) -> str: |  | ||||||
|             if isinstance(self._ti, EnumType): |  | ||||||
|                 return f"buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>({element}), true);" |  | ||||||
|             return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" |  | ||||||
|  |  | ||||||
|         # If skip_zero is enabled, wrap encoding in a zero check |         # If skip_zero is enabled, wrap encoding in a zero check | ||||||
|         if self.skip_zero: |         if self.skip_zero: | ||||||
|             if self.is_define: |             if self.is_define: | ||||||
|                 # When using a define, we need to use a loop-based approach |                 # When using a define, we need to use a loop-based approach | ||||||
|                 o = f"for (const auto &it : this->{self.field_name}) {{\n" |                 o = f"for (const auto &it : this->{self.field_name}) {{\n" | ||||||
|                 o += "  if (it != 0) {\n" |                 o += "  if (it != 0) {\n" | ||||||
|                 o += f"    {encode_element('it')}\n" |                 o += f"    {self._encode_element('it')}\n" | ||||||
|                 o += "  }\n" |                 o += "  }\n" | ||||||
|                 o += "}" |                 o += "}" | ||||||
|                 return o |                 return o | ||||||
| @@ -1132,7 +1137,7 @@ class FixedArrayRepeatedType(TypeInfo): | |||||||
|                 [f"this->{self.field_name}[{i}] != 0" for i in range(self.array_size)] |                 [f"this->{self.field_name}[{i}] != 0" for i in range(self.array_size)] | ||||||
|             ) |             ) | ||||||
|             encode_lines = [ |             encode_lines = [ | ||||||
|                 f"  {encode_element(f'this->{self.field_name}[{i}]')}" |                 f"  {self._encode_element(f'this->{self.field_name}[{i}]')}" | ||||||
|                 for i in range(self.array_size) |                 for i in range(self.array_size) | ||||||
|             ] |             ] | ||||||
|             return f"if ({non_zero_checks}) {{\n" + "\n".join(encode_lines) + "\n}" |             return f"if ({non_zero_checks}) {{\n" + "\n".join(encode_lines) + "\n}" | ||||||
| @@ -1140,23 +1145,23 @@ class FixedArrayRepeatedType(TypeInfo): | |||||||
|         # When using a define, always use loop-based approach |         # When using a define, always use loop-based approach | ||||||
|         if self.is_define: |         if self.is_define: | ||||||
|             o = f"for (const auto &it : this->{self.field_name}) {{\n" |             o = f"for (const auto &it : this->{self.field_name}) {{\n" | ||||||
|             o += f"  {encode_element('it')}\n" |             o += f"  {self._encode_element('it')}\n" | ||||||
|             o += "}" |             o += "}" | ||||||
|             return o |             return o | ||||||
|  |  | ||||||
|         # Unroll small arrays for efficiency |         # Unroll small arrays for efficiency | ||||||
|         if self.array_size == 1: |         if self.array_size == 1: | ||||||
|             return encode_element(f"this->{self.field_name}[0]") |             return self._encode_element(f"this->{self.field_name}[0]") | ||||||
|         if self.array_size == 2: |         if self.array_size == 2: | ||||||
|             return ( |             return ( | ||||||
|                 encode_element(f"this->{self.field_name}[0]") |                 self._encode_element(f"this->{self.field_name}[0]") | ||||||
|                 + "\n  " |                 + "\n  " | ||||||
|                 + encode_element(f"this->{self.field_name}[1]") |                 + self._encode_element(f"this->{self.field_name}[1]") | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         # Use loops for larger arrays |         # Use loops for larger arrays | ||||||
|         o = f"for (const auto &it : this->{self.field_name}) {{\n" |         o = f"for (const auto &it : this->{self.field_name}) {{\n" | ||||||
|         o += f"  {encode_element('it')}\n" |         o += f"  {self._encode_element('it')}\n" | ||||||
|         o += "}" |         o += "}" | ||||||
|         return o |         return o | ||||||
|  |  | ||||||
| @@ -1230,6 +1235,66 @@ class FixedArrayRepeatedType(TypeInfo): | |||||||
|         return underlying_size * self.array_size |         return underlying_size * self.array_size | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FixedArrayWithLengthRepeatedType(FixedArrayRepeatedType): | ||||||
|  |     """Special type for fixed-size repeated fields with variable length tracking. | ||||||
|  |  | ||||||
|  |     Similar to FixedArrayRepeatedType but generates an additional length field | ||||||
|  |     to track how many elements are actually in use. Only encodes/sends elements | ||||||
|  |     up to the current length. | ||||||
|  |  | ||||||
|  |     Fixed arrays with length are only supported for encoding (SOURCE_SERVER) since | ||||||
|  |     we cannot control how many items we receive when decoding. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def public_content(self) -> list[str]: | ||||||
|  |         # Return both the array and the length field | ||||||
|  |         return [ | ||||||
|  |             f"{self.cpp_type} {self.field_name}{{}};", | ||||||
|  |             f"uint16_t {self.field_name}_len{{0}};", | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def encode_content(self) -> str: | ||||||
|  |         # Always use a loop up to the current length | ||||||
|  |         o = f"for (uint16_t i = 0; i < this->{self.field_name}_len; i++) {{\n" | ||||||
|  |         o += f"  {self._encode_element(f'this->{self.field_name}[i]')}\n" | ||||||
|  |         o += "}" | ||||||
|  |         return o | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def dump_content(self) -> str: | ||||||
|  |         # Dump only the active elements | ||||||
|  |         o = f"for (uint16_t i = 0; i < this->{self.field_name}_len; i++) {{\n" | ||||||
|  |         # Check if underlying type can use dump_field | ||||||
|  |         if type(self._ti).can_use_dump_field(): | ||||||
|  |             o += f'  dump_field(out, "{self.name}", {self._ti.dump_field_value(f"this->{self.field_name}[i]")}, 4);\n' | ||||||
|  |         else: | ||||||
|  |             o += f'  out.append("  {self.name}: ");\n' | ||||||
|  |             o += indent(self._ti.dump(f"this->{self.field_name}[i]")) + "\n" | ||||||
|  |             o += '  out.append("\\n");\n' | ||||||
|  |         o += "}" | ||||||
|  |         return o | ||||||
|  |  | ||||||
|  |     def get_size_calculation(self, name: str, force: bool = False) -> str: | ||||||
|  |         # Calculate size only for active elements | ||||||
|  |         o = f"for (uint16_t i = 0; i < {name}_len; i++) {{\n" | ||||||
|  |         o += f"  {self._ti.get_size_calculation(f'{name}[i]', True)}\n" | ||||||
|  |         o += "}" | ||||||
|  |         return o | ||||||
|  |  | ||||||
|  |     def get_estimated_size(self) -> int: | ||||||
|  |         # For fixed arrays with length, estimate based on typical usage | ||||||
|  |         # Assume on average half the array is used | ||||||
|  |         underlying_size = self._ti.get_estimated_size() | ||||||
|  |         if self.is_define: | ||||||
|  |             # When using a define, estimate 8 elements as typical | ||||||
|  |             return underlying_size * 8 | ||||||
|  |         return underlying_size * ( | ||||||
|  |             self.array_size // 2 if self.array_size > 2 else self.array_size | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
| class RepeatedTypeInfo(TypeInfo): | class RepeatedTypeInfo(TypeInfo): | ||||||
|     def __init__(self, field: descriptor.FieldDescriptorProto) -> None: |     def __init__(self, field: descriptor.FieldDescriptorProto) -> None: | ||||||
|         super().__init__(field) |         super().__init__(field) | ||||||
| @@ -1711,6 +1776,19 @@ def build_message_type( | |||||||
|                 f"since we cannot trust or control the number of items received from clients." |                 f"since we cannot trust or control the number of items received from clients." | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |         # Validate that fixed_array_with_length_define is only used in encode-only messages | ||||||
|  |         if ( | ||||||
|  |             needs_decode | ||||||
|  |             and field.label == 3 | ||||||
|  |             and get_field_opt(field, pb.fixed_array_with_length_define) is not None | ||||||
|  |         ): | ||||||
|  |             raise ValueError( | ||||||
|  |                 f"Message '{desc.name}' uses fixed_array_with_length_define on field '{field.name}' " | ||||||
|  |                 f"but has source={SOURCE_NAMES[source]}. " | ||||||
|  |                 f"Fixed arrays with length are only supported for SOURCE_SERVER (encode-only) messages " | ||||||
|  |                 f"since we cannot trust or control the number of items received from clients." | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         ti = create_field_type_info(field, needs_decode, needs_encode) |         ti = create_field_type_info(field, needs_decode, needs_encode) | ||||||
|  |  | ||||||
|         # Skip field declarations for fields that are in the base class |         # Skip field declarations for fields that are in the base class | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user