@@ -381,44 +381,74 @@ async fn logger_task(global_state: GlobalState) {
381381 loop {
382382 Timer :: after_millis ( LOG_SEND_INTERVAL_MS ) . await ;
383383
384- let mut tmp_logs: Vec < String > = Vec :: new ( ) ;
385- for _ in 0 ..consts:: LOG_BATCH_SIZE {
386- match utils:: logger:: LOGS_CHANNEL . try_receive ( ) {
387- Ok ( msg) => tmp_logs. push ( msg) ,
388- Err ( _) => break ,
389- }
384+ if ota_state ( ) || sleep_state ( ) {
385+ // Drain and discard so the channel does not fill up.
386+ while utils:: logger:: LOGS_CHANNEL . try_receive ( ) . is_ok ( ) { }
387+ continue ;
390388 }
391389
392- if ota_state ( ) || sleep_state ( ) {
390+ // How many entries can we drain this tick?
391+ let available = utils:: logger:: LOGS_CHANNEL . len ( ) . min ( consts:: LOG_BATCH_SIZE ) ;
392+ if available == 0 {
393+ continue ;
394+ }
395+
396+ // Binary log packet layout:
397+ // [1] packet type = 0x01
398+ // [8] current_time (u64 LE)
399+ // [1] entry count (u8)
400+ // per entry:
401+ // [2] byte length (u16 LE)
402+ // [N] UTF-8 string
403+ //
404+ // Pre-compute worst-case payload size and trim the count until it fits
405+ // in available heap, keeping MIN_HEAP_REMEANING as safety margin.
406+ // This is a single, non-growing allocation — no serde_json, no copies.
407+ const HEADER : usize = 10 ; // 1 + 8 + 1
408+ const ENTRY_OVERHEAD : usize = 2 ; // u16 length prefix
409+ let mut count = available;
410+ loop {
411+ if count == 0 {
412+ break ;
413+ }
414+ let max_payload =
415+ HEADER + count * ( ENTRY_OVERHEAD + utils:: logger:: LOG_MSG_MAX_LEN ) ;
416+ if esp_alloc:: HEAP . free ( )
417+ > max_payload + utils:: logger:: MIN_HEAP_REMEANING
418+ {
419+ break ;
420+ }
421+ count -= 1 ;
422+ }
423+ if count == 0 {
393424 continue ;
394425 }
395426
396- if !tmp_logs. is_empty ( ) {
397- tmp_logs. reverse ( ) ;
398-
399- // Drop oldest entries (end of vec after reverse) until the estimated
400- // serialised JSON fits comfortably in available heap. Each pop()
401- // immediately frees the String's heap allocation, raising HEAP.free().
402- while tmp_logs. len ( ) > 1 {
403- // Estimate: each entry contributes its byte length + 3 (quotes +
404- // comma), plus ~80 bytes of JSON wrapper. Multiply by 2 to cover
405- // serde_json's internal growth doubling.
406- let estimated: usize =
407- tmp_logs. iter ( ) . map ( |s| s. len ( ) + 3 ) . sum :: < usize > ( ) * 2 + 80 ;
408- if esp_alloc:: HEAP . free ( ) > estimated + utils:: logger:: MIN_HEAP_REMEANING {
409- break ;
427+ let capacity = HEADER + count * ( ENTRY_OVERHEAD + utils:: logger:: LOG_MSG_MAX_LEN ) ;
428+ let mut payload: Vec < u8 > = Vec :: with_capacity ( capacity) ;
429+
430+ let current_time = unsafe { crate :: stackmat:: CURRENT_TIME } ;
431+ payload. push ( 0x01_u8 ) ; // packet type: Logs
432+ payload. extend_from_slice ( & current_time. to_le_bytes ( ) ) ;
433+ payload. push ( count as u8 ) ; // placeholder; corrected below
434+
435+ let mut actual_count: u8 = 0 ;
436+ for _ in 0 ..count {
437+ match utils:: logger:: LOGS_CHANNEL . try_receive ( ) {
438+ Ok ( msg) => {
439+ let b = msg. as_bytes ( ) ;
440+ payload. extend_from_slice ( & ( b. len ( ) as u16 ) . to_le_bytes ( ) ) ;
441+ payload. extend_from_slice ( b) ;
442+ actual_count += 1 ;
443+ // `msg` (heapless::String, BSS-backed) is dropped here
410444 }
411- tmp_logs . pop ( ) ;
445+ Err ( _ ) => break ,
412446 }
447+ }
448+ payload[ 9 ] = actual_count; // correct the count byte
413449
414- ws:: send_packet ( structs:: TimerPacket {
415- tag : None ,
416- data : structs:: TimerPacketInner :: Logs {
417- current_time : Some ( unsafe { crate :: stackmat:: CURRENT_TIME } ) ,
418- logs : tmp_logs,
419- } ,
420- } )
421- . await ;
450+ if actual_count > 0 {
451+ ws:: send_binary ( payload) . await ;
422452 }
423453
424454 #[ cfg( not( feature = "release_build" ) ) ]
0 commit comments