Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Setup and Tap

Before we can construct our ControllerCore structure, we need to have the allocations of the components ready. We choose not to pass these around beyond constructing them into a single location, since we may run into borrow violations if we hand the references out too liberally, like we have seen in our previous integration attempts.

This becomes even more complicated by the fact that when we commit our Battery Controller object to the battery service, we pass ownership to it -- and therefore lose access to our own construction. The solution here is to not give the battery service control of our Battery directly, but to give it a BatteryAdapter that looks like a battery, but instead simply forwards all of its actions to our ControllerCore. We call this "tapping" the service. In the ControllerCore we have access to not only our own battery, but also our charger and thermal components, so we can conduct our integration in a unified way. That said, we will still avoid tightly-coupled access between components as much as possible in favor of using messaging, because this pattern fosters better modularity.

In a view

The diagram below shows the ownership and message flow at a glance:

flowchart LR

  %% --- UI ---
  subgraph UI[UI]
    direction TB
    User[User]
    Obs[SystemObserver]
    Rend[DisplayRenderer]
  end

  %% --- Channels ---
  subgraph Channels[Channels]
    direction TB
    IChan[InteractionChannel]
    DChan[DisplayChannel]
    Bc[BatteryChannel]
    Cc[ChargerChannel]
    Tc[ThermalChannel]
  end

  %% --- Service ---
  subgraph Service[Service]
    direction TB
    W[Wrapper] --> A[BatteryAdapter]
  end

  %% --- Core ---
  subgraph Core[Core]
    direction TB
    CC[ControllerCore]
    B[MockBatteryController]
    C[MockChargerController]
    S[Thermal Service Sensor]
    F[Thermal Service Fan]
    CC --> B & C & S & F
  end

  %% --- Wiring ---
  User --> IChan
  IChan --> CC

  A --> CC

  CC --> Obs
  Obs --> DChan
  DChan --> Rend

  CC --> Bc
  CC --> Cc
  CC --> Tc

The setup_and_tap code

Create setup_and_tap.rs and give it this content to start:

#![allow(unused)]
fn main() {
use embassy_executor::Spawner;
use embassy_time::Duration;
use static_cell::StaticCell;
use embassy_sync::once_lock::OnceLock;
use ec_common::mutex::{Mutex, RawMutex};

use crate::entry::{Shared, BATTERY_DEV_NUM, CHARGER_DEV_NUM, SENSOR_DEV_NUM, FAN_DEV_NUM};
use crate::controller_core::ControllerCore;

use embedded_services::init;
use embedded_services::power::policy::register_device;
use embedded_services::power::policy::DeviceId;
use embedded_services::power::policy::device::Device as PolicyDevice;

use mock_battery::mock_battery_device::MockBatteryDevice;
use mock_battery::mock_battery_controller::MockBatteryController;

use mock_charger::mock_charger_device::MockChargerDevice;
use mock_charger::mock_charger_controller::MockChargerController;
use embedded_services::power::policy::charger::Device as ChargerDevice; // disambiguate from other device types
use embedded_services::power::policy::policy::register_charger;
use embedded_services::power::policy::charger::ChargerId;

use mock_thermal::mock_sensor_device::MockSensorDevice;
use mock_thermal::mock_fan_device::MockFanDevice;
use mock_thermal::mock_sensor_controller::MockSensorController;
use mock_thermal::mock_fan_controller::MockFanController;

use battery_service::wrapper::Wrapper;

use crate::battery_adapter::BatteryAdapter;

use thermal_service as ts;
use ts::sensor as tss;
use ts::fan   as tsf;

pub const INTERNAL_SAMPLE_BUF_LENGTH:usize =  16; // must be a power of 2

// ---------- statics that must live for 'static tasks ----------
static BATTERY_WRAPPER: StaticCell<Wrapper<'static, BatteryAdapter>> = StaticCell::new();

static BATTERY_DEVICE: StaticCell<MockBatteryDevice> = StaticCell::new();
static BATTERY_POLICY_DEVICE: StaticCell<PolicyDevice> = StaticCell::new();

static CHARGER_DEVICE: StaticCell<MockChargerDevice> = StaticCell::new();
static CHARGER_POLICY_DEVICE: StaticCell<MockChargerDevice> = StaticCell::new();
static CHARGER_SERVICE_DEVICE: OnceLock<ChargerDevice> = OnceLock::new();


static SENSOR_DEVICE: StaticCell<MockSensorDevice> = StaticCell::new();
static FAN_DEVICE:    StaticCell<MockFanDevice>    = StaticCell::new();

static TS_SENSOR: StaticCell<tss::Sensor<mock_thermal::mock_sensor_controller::MockSensorController, {INTERNAL_SAMPLE_BUF_LENGTH}>> = StaticCell::new();
static TS_FAN:    StaticCell<tsf::Fan<mock_thermal::mock_fan_controller::MockFanController, {INTERNAL_SAMPLE_BUF_LENGTH}>>          = StaticCell::new();

// Generate Embassy tasks for concrete controller types.
ts::impl_sensor_task!(
    thermal_sensor_task,
    mock_thermal::mock_sensor_controller::MockSensorController,
    INTERNAL_SAMPLE_BUF_LENGTH
);

ts::impl_fan_task!(
    thermal_fan_task,
    mock_thermal::mock_fan_controller::MockFanController,
    INTERNAL_SAMPLE_BUF_LENGTH
);

use crate::config::policy_config::ThermalPolicyCfg;


pub fn make_sensor_profile(p: &ThermalPolicyCfg) -> tss::Profile {
    tss::Profile {
        // thresholds
        warn_low_threshold:  p.temp_low_on_c,
        warn_high_threshold: p.temp_high_on_c,
        prochot_threshold:   p.sensor_prochot_c,
        crt_threshold:       p.sensor_crt_c,

        // debouncing
        hysteresis: p.sensor_hysteresis_c,

        // sampling
        sample_period:         p.sensor_sample_period_ms,
        fast_sample_period:    p.sensor_fast_sample_period_ms,
        fast_sampling_threshold: p.sensor_fast_sampling_threshold_c,

        // misc
        offset: 0.0,
        retry_attempts: 5,

        ..Default::default()
    }
}

pub fn make_fan_profile(p: &ThermalPolicyCfg, sensor_id: tss::DeviceId) -> tsf::Profile {
    tsf::Profile {
        sensor_id,

        // ramp shape
        on_temp:    p.fan_on_temp_c,
        ramp_temp:  p.fan_ramp_temp_c,
        max_temp:   p.fan_max_temp_c,
        hysteresis: p.fan_hyst_c,

        // control
        auto_control: p.fan_auto_control,

        // sampling/update cadences
        sample_period: p.fan_sample_period_ms,
        update_period: p.fan_update_period_ms,

        ..Default::default()
    }
}

/// Initialize registration of all the integration components
#[embassy_executor::task]
pub async fn setup_and_tap_task(spawner: Spawner, shared: &'static Shared) {
    println!("⚙️ Initializing embedded-services");
    init().await;

    println!("⚙️ Spawning battery service task");
    spawner.spawn(battery_service::task()).unwrap();

    // ----------------- Device/controller construction -----------------
    let battery_dev = BATTERY_DEVICE.init(MockBatteryDevice::new(DeviceId(BATTERY_DEV_NUM)));
    let battery_policy_dev = BATTERY_POLICY_DEVICE.init(PolicyDevice::new(DeviceId(BATTERY_DEV_NUM)));

    // Build the battery controller locally and MOVE it into the wrapper below.
    // (No StaticCell needed for the controller since the wrapper will own it.)
    let battery_controller = MockBatteryController::new(battery_dev);

    // Similar for others, although they are not moved into wrapper
    let charger_dev = CHARGER_DEVICE.init(MockChargerDevice::new(ChargerId(CHARGER_DEV_NUM)));
    let charger_policy_dev = CHARGER_POLICY_DEVICE.init(MockChargerDevice::new(ChargerId(CHARGER_DEV_NUM)));
    let charger_controller = MockChargerController::new(charger_dev);


    // Thermal (controllers own their devices)
    let sensor_dev = SENSOR_DEVICE.init(MockSensorDevice::new(DeviceId(SENSOR_DEV_NUM)));
    let fan_dev    = FAN_DEVICE.init(MockFanDevice::new(DeviceId(FAN_DEV_NUM)));

    // Build profiles from config
    let thermal_cfg = ThermalPolicyCfg::default();
    let sensor_profile = make_sensor_profile(&thermal_cfg);
    let fan_profile    = make_fan_profile(&thermal_cfg,
                                      tss::DeviceId(SENSOR_DEV_NUM as u8));
    // create controllers
    let sensor_controller = MockSensorController::new(sensor_dev);
    let fan_controller    = MockFanController::new(fan_dev);

    // build ODP registration-ready wrappers for these    
    let sensor = TS_SENSOR.init(tss::Sensor::new(
        tss::DeviceId(SENSOR_DEV_NUM as u8),
        sensor_controller,
        sensor_profile,
    ));
    let fan = TS_FAN.init(tsf::Fan::new(
        tsf::DeviceId(FAN_DEV_NUM as u8),
        fan_controller,
        fan_profile,
    ));

    println!("🌡️ Initializing thermal service");
    thermal_service::init().await.unwrap(); 

    // Register with the thermal service
    println!("🧩 Registering sensor device to thermal service...");
    ts::register_sensor(sensor.device()).await.unwrap();
    println!("🧩 Registering fan device to thermal service...");
    ts::register_fan(fan.device()).await.unwrap();


    // Spawn the ODP thermal tasks (these tasks created by ts::impl_ macros at module scope above)
    spawner.must_spawn(thermal_sensor_task(sensor));
    spawner.must_spawn(thermal_fan_task(fan));
    // To support MPTF/host messages:
    spawner.must_spawn(ts::mptf::handle_requests());

    let charger_service_device: &'static ChargerDevice = CHARGER_SERVICE_DEVICE.get_or_init(|| ChargerDevice::new(ChargerId(CHARGER_DEV_NUM)));

    // Then use these to create our ControllerTap handler, which isolates ownership of all but the battery, which is
    // owned by the Wrapper.  We can access the other "real" controllers upon battery message receipts by the Tap.
    // We must still stick to message passing to communicate between components to preserve modularity.
    let controller_core = ControllerCore::new(
        battery_controller, charger_controller, sensor, fan,
        charger_service_device, 
        shared.battery_channel,shared.charger_channel,shared.thermal_channel,shared.interaction_channel,
        shared.observer,
    );

    static TAP_CELL: StaticCell<Mutex<RawMutex, ControllerCore>> = StaticCell::new();
    let core_mutex: &'static Mutex<RawMutex, ControllerCore> = TAP_CELL.init(Mutex::new(controller_core));
    let battery_adapter = BatteryAdapter::new(core_mutex);


    // ----------------- Battery wrapper -----------------
    println!("⚙️ Spawning battery wrapper task");
    let wrapper = BATTERY_WRAPPER.init(Wrapper::new(
        shared.battery_fuel,        // &'static BatteryDevice, provided by Instances
        battery_adapter            // move ownership into the wrapper
    ));
    spawner.spawn(battery_wrapper_task(wrapper)).unwrap();

    // Registrations
    println!("🧩 Registering battery device...");
    register_device(battery_policy_dev).await.unwrap();

    println!("🧩 Registering charger device...");
    register_charger(charger_policy_dev).await.unwrap();

    // ----------------- Fuel gauge / ready -----------------
    println!("🔌 Initializing battery fuel gauge service...");
    battery_service::register_fuel_gauge(&shared.battery_fuel).await.unwrap();

    spawner.spawn(battery_start_task()).unwrap();

    // insure launched tasks have started running before we execute request
    embassy_futures::yield_now().await;

    // Turn on auto control for fan
    println!("💡Turning on Fan EnableAutoControl...");
    let _ = fan.device().execute_request(thermal_service::fan::Request::EnableAutoControl).await;

    // signal that the battery fuel service is ready
    shared.battery_ready.signal();

    println!("Setup and Tap calling ControllerCore::start...");
    ControllerCore::start(core_mutex, spawner);

}

}

This starts out by allocating and creating the components that we will need, starting with the aforementioned BatteryAdapter, which we will implement in a moment, and creating the BatteryWrapper with this in mind.

It then creates the battery, charger, sensor, and fan components. You may notice that in doing so for the battery and charger, we create both a DEVICE and a POLICY_DEVICE for each. Both of these Device type wrappers are identical per component. One is used to create the controller, and one is used to register the device with the service. Since these are tied by Id designation, they are equivalent, and since we can't pass a single instance twice without incurring a borrow violation, we use this technique.

For the sensor and fan, we wrap our controllers in the TS_SENSOR and TS_FAN allocated statics. These wrappers contain the controllers and device ids, and the profile configurations needed to enact the ODP prescribed thermal-service handling. We then init the thermal service and use these wrappers to register these thermal components into it.

Note the additional items at module-scope to support this. We call upon the ts::impl_sensor_task! and ts::impl_fan_task! macros that will generate a task function we can spawn to to support these services. We also have defined helper functions that allow us to map our policy configuration values to the ODP policy structures for these services.

This brings us to the construction of the ControllerCore. Here, we give it all of the components, plus the comm channels that were shared from our earlier allocations in entry.rs. We also see here we are passing references to a new channel interaction_channel, and the SystemObserver, neither of which we have created yet.

Once we get our ControllerCore instance created, we wrap it into a mutex that we stash into a StaticCell so that we have portable access to this structure.

The remainder of the setup_and_tap_task proceeds with registration and then spawning the execution tasks.


Mind the namespaces

Note the use statements and the namespaces for similarly named types (Device, ChargerId, register_device, register_charger, register_fan, etc.) The ODP supported services generally use type-specific but similar sounding structures.
It is easy to include the wrong 'version' of a named struct from a different namespace. If you apply these incorrectly, you will get unexpected results.