11use anyhow:: { Context as _, Result , anyhow, bail} ;
2+ mod fan_controller;
3+ mod thermal_task;
4+
25use async_trait:: async_trait;
36use futures:: sink:: SinkExt ;
47use std:: {
@@ -48,6 +51,9 @@ use super::{
4851 pattern:: { Match , StringMatch } ,
4952} ;
5053
54+ use fan_controller:: { FanControllerConfig , ThermalReadings } ;
55+ use thermal_task:: ThermalTask ;
56+
5157/// Adapter implementing `AsicEnable` for Bitaxe's GPIO-based reset control.
5258struct BitaxeAsicEnable {
5359 /// Reset pin (directly controls nRST on the BM1370)
@@ -125,8 +131,8 @@ pub struct BitaxeBoard {
125131 asic_nrst : Option < BitaxeRawGpioPin > ,
126132 /// I2C bus controller
127133 i2c : BitaxeRawI2c ,
128- /// Fan controller (board-controlled only, not shared with thread)
129- fan_controller : Option < Emc2101 < BitaxeRawI2c > > ,
134+ /// Concurrent EMC2101 handle
135+ emc2101 : Option < Arc < Mutex < Emc2101 < BitaxeRawI2c > > > > ,
130136 /// Voltage regulator (shared with thread, cached state)
131137 regulator : Option < Arc < Mutex < Tps546 < BitaxeRawI2c > > > > ,
132138 /// Writer for sending commands to chips (transferred to hash thread)
@@ -147,6 +153,10 @@ pub struct BitaxeBoard {
147153 /// Channel for publishing board telemetry to the API server.
148154 /// Taken by `spawn_stats_monitor` which publishes periodic snapshots.
149155 telemetry_tx : Option < watch:: Sender < BoardTelemetry > > ,
156+ /// Fan controller configuration (target temperature, etc.)
157+ fan_config : FanControllerConfig ,
158+ /// Handles for the thermal monitor async task
159+ thermal_task_handle : Option < tokio:: task:: JoinHandle < ( ) > > ,
150160}
151161
152162impl BitaxeBoard {
@@ -195,7 +205,7 @@ impl BitaxeBoard {
195205 control_channel,
196206 asic_nrst : None ,
197207 i2c,
198- fan_controller : None ,
208+ emc2101 : None ,
199209 regulator : None ,
200210 data_writer : Some ( FramedWrite :: new ( data_writer, bm13xx:: FrameCodec ) ) ,
201211 data_reader : Some ( FramedRead :: new ( tracing_reader, bm13xx:: FrameCodec ) ) ,
@@ -205,6 +215,8 @@ impl BitaxeBoard {
205215 stats_task_handle : None ,
206216 serial_number,
207217 telemetry_tx : Some ( telemetry_tx) ,
218+ fan_config : FanControllerConfig :: default ( ) ,
219+ thermal_task_handle : None ,
208220 } )
209221 }
210222
@@ -451,36 +463,6 @@ impl BitaxeBoard {
451463 }
452464 }
453465
454- /// Initialize the fan controller
455- async fn init_fan_controller ( & mut self ) -> Result < ( ) > {
456- // Clone the I2C bus for the fan controller
457- let fan_i2c = self . i2c . clone ( ) ;
458- let mut fan = Emc2101 :: new ( fan_i2c) ;
459-
460- // Initialize the EMC2101
461- match fan. init ( ) . await {
462- Ok ( ( ) ) => {
463- // Set fan to full speed until closed-loop control is implemented
464- match fan. set_fan_speed ( Percent :: FULL ) . await {
465- Ok ( ( ) ) => {
466- debug ! ( "Fan speed set to 100%" ) ;
467- }
468- Err ( e) => {
469- warn ! ( "Failed to set fan speed: {}" , e) ;
470- }
471- }
472-
473- self . fan_controller = Some ( fan) ;
474- Ok ( ( ) )
475- }
476- Err ( e) => {
477- warn ! ( "Failed to initialize EMC2101 fan controller: {}" , e) ;
478- // Continue without fan control - not critical for operation
479- Ok ( ( ) )
480- }
481- }
482- }
483-
484466 /// Generate frequency ramp steps for smooth PLL transitions
485467 ///
486468 /// Calculates PLL configurations for each frequency step from start to target.
@@ -620,7 +602,6 @@ impl BitaxeBoard {
620602 . await
621603 . context ( "failed to set I2C frequency" ) ?;
622604
623- self . init_fan_controller ( ) . await ?;
624605 self . init_power_controller ( ) . await ?;
625606
626607 tokio:: time:: sleep ( Duration :: from_millis ( 500 ) ) . await ;
@@ -668,8 +649,14 @@ impl BitaxeBoard {
668649 // Put chip back in reset
669650 self . hold_in_reset ( ) . await ?;
670651
652+ let ( thermal_readings_tx, thermal_readings_rx) =
653+ watch:: channel :: < Option < ThermalReadings > > ( None ) ;
654+
655+ // Spawn thermal monitoring task
656+ self . spawn_thermal_task ( thermal_readings_tx) . await ?;
657+
671658 // Spawn statistics monitoring task
672- self . spawn_stats_monitor ( ) ;
659+ self . spawn_stats_monitor ( thermal_readings_rx ) ;
673660
674661 Ok ( ( ) )
675662 }
@@ -680,10 +667,10 @@ impl BitaxeBoard {
680667 }
681668
682669 /// Spawn a task to periodically log and publish board telemetry.
683- fn spawn_stats_monitor ( & mut self ) {
684- // Clone data needed for the monitoring task
685- let i2c = self . i2c . clone ( ) ;
686-
670+ fn spawn_stats_monitor (
671+ & mut self ,
672+ thermal_readings_rx : watch :: Receiver < Option < ThermalReadings > > ,
673+ ) {
687674 // Clone the regulator Arc for stats monitoring
688675 let regulator = self
689676 . regulator
@@ -710,9 +697,6 @@ impl BitaxeBoard {
710697 let mut interval = tokio:: time:: interval ( STATS_INTERVAL ) ;
711698 interval. set_missed_tick_behavior ( tokio:: time:: MissedTickBehavior :: Skip ) ;
712699
713- // Create fan controller for the stats task
714- let mut fan_ctrl = Emc2101 :: new ( i2c) ;
715-
716700 const LOG_INTERVAL : Duration = Duration :: from_secs ( 30 ) ;
717701 let mut last_log = tokio:: time:: Instant :: now ( ) ;
718702
@@ -722,11 +706,13 @@ impl BitaxeBoard {
722706 loop {
723707 interval. tick ( ) . await ;
724708
725- // -- Read sensor values --
709+ // -- Get latest thermal readings --
726710
727- let asic_temp = fan_ctrl. get_external_temperature ( ) . await . ok ( ) ;
728- let fan_percent = fan_ctrl. get_fan_speed ( ) . await . ok ( ) . map ( u8:: from) ;
729- let fan_rpm = fan_ctrl. get_rpm ( ) . await . ok ( ) ;
711+ let thermal_readings = thermal_readings_rx
712+ . borrow ( )
713+ . as_ref ( )
714+ . copied ( )
715+ . unwrap_or_default ( ) ;
730716
731717 let ( vin_mv, vout_mv, iout_ma, power_mw, vr_temp) = {
732718 let mut reg = regulator. lock ( ) . await ;
@@ -769,14 +755,16 @@ impl BitaxeBoard {
769755 serial : board_serial. clone ( ) ,
770756 fans : vec ! [ Fan {
771757 name: "fan" . into( ) ,
772- rpm: fan_rpm,
773- percent: fan_percent,
758+ rpm: thermal_readings . fan_rpm,
759+ percent: thermal_readings . fan_percent,
774760 target_percent: None ,
775761 } ] ,
776762 temperatures : vec ! [
777763 TemperatureSensor {
778764 name: "asic" . into( ) ,
779- temperature: asic_temp. map( Temperature :: from_celsius) ,
765+ temperature: thermal_readings
766+ . asic_temp_c
767+ . map( Temperature :: from_celsius) ,
780768 } ,
781769 TemperatureSensor {
782770 name: "vr" . into( ) ,
@@ -807,9 +795,9 @@ impl BitaxeBoard {
807795 info ! (
808796 board = %board_model,
809797 serial = ?board_serial,
810- asic_temp_c = ?asic_temp ,
811- fan_percent = ?fan_percent,
812- fan_rpm = ?fan_rpm,
798+ asic_temp_c = ?thermal_readings . asic_temp_c ,
799+ fan_percent = ?thermal_readings . fan_percent,
800+ fan_rpm = ?thermal_readings . fan_rpm,
813801 vr_temp_c = ?vr_temp,
814802 power_w = ?power_mw. map( |mw| mw as f32 / 1000.0 ) ,
815803 current_a = ?iout_ma. map( |ma| ma as f32 / 1000.0 ) ,
@@ -823,6 +811,27 @@ impl BitaxeBoard {
823811
824812 self . stats_task_handle = Some ( handle) ;
825813 }
814+
815+ /// Initialize fan hardware and spawn the thermal control loop task.
816+ async fn spawn_thermal_task (
817+ & mut self ,
818+ thermal_readings_tx : watch:: Sender < Option < ThermalReadings > > ,
819+ ) -> Result < ( ) > {
820+ let fan_hw = Arc :: new ( Mutex :: new ( Emc2101 :: new ( self . i2c . clone ( ) ) ) ) ;
821+
822+ let thermal_task =
823+ ThermalTask :: new ( fan_hw. clone ( ) , self . fan_config . clone ( ) , thermal_readings_tx) ;
824+
825+ let handle = thermal_task
826+ . start ( )
827+ . await
828+ . context ( "failed to start thermal task" ) ?;
829+
830+ self . emc2101 = Some ( fan_hw) ;
831+ self . thermal_task_handle = Some ( handle) ;
832+
833+ Ok ( ( ) )
834+ }
826835}
827836
828837#[ async_trait]
@@ -857,12 +866,27 @@ impl Board for BitaxeBoard {
857866 }
858867 }
859868
869+ // Cancel the thermal monitoring task
870+ if let Some ( handle) = self . thermal_task_handle . take ( ) {
871+ handle. abort ( ) ;
872+ }
873+
860874 // Reduce fan speed (no more heat generation)
861- if let Some ( ref mut fan) = self . fan_controller {
862- let shutdown_speed = Percent :: new_clamped ( 25 ) ;
863- if let Err ( e) = fan. set_fan_speed ( shutdown_speed) . await {
864- warn ! ( "Failed to set fan speed: {}" , e) ;
865- }
875+ if let Some ( ref emc2101_handle) = self . emc2101 {
876+ match emc2101_handle
877+ . lock ( )
878+ . await
879+ . set_fan_speed ( Percent :: new_clamped (
880+ self . fan_config . fan_speed_min_pct . round ( ) as u8 ,
881+ ) )
882+ . await
883+ {
884+ Ok ( ( ) ) => debug ! (
885+ "Fan set to {}%" ,
886+ self . fan_config. fan_speed_min_pct. round( ) as u8
887+ ) ,
888+ Err ( e) => warn ! ( "Failed to set the fan speed: {}" , e) ,
889+ } ;
866890 }
867891
868892 // Cancel the statistics monitoring task
0 commit comments