Create a custom power button handler
IBM has specific requirements on how the power button must behave, which
are different than what the button handler currently does.
These requirements are:
If power is off:
- A button press will power on as long as the BMC is
in the ready state.
If power is on:
- A button press less than 4s won't do anything.
- At 4s, issue a host power off and start a 10s timer.
- If the button is released within that 10s and not pressed
again, continue with the host power off.
- If the button is released within that 10s and also
pressed again in that 10s, do a hard power (chassis)
off.
- If the button is pressed throughout that 10s
issue a hard power off.
Instead of trying to integrate this behavior into the main button
handler code using even more #ifdefs in various spots, this commit
creates the concept of custom power button handlers, and then implements
one. This makes it less likely it could subtly break when changes are
made to the default code.
A 'power-button-handler' meson option is used to select the handler,
where the default uses the current behavior. A
PowerButtonHandlerFactory then creates the appropriate instance of the
handler selected by that option. If a handler is found, then the
default matches/callbacks won't be done and the handler can deal with
them as it sees fit.
Handlers are derived from a PowerButtonHandler abstract base class that
has 2 pure virtual functions:
- void pressed();
- void released(uint64_t pressTimeMS);
It will register for the power button pressed/released signals and then
call these overridden functions appropriately.
This new handler is implemented in a HostThenChassisPowerOff class.
Change-Id: I3a1df688c4393b4643d42e91c075379f9a266eef
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
diff --git a/inc/button_handler.hpp b/inc/button_handler.hpp
index e398a0a..1332a0f 100755
--- a/inc/button_handler.hpp
+++ b/inc/button_handler.hpp
@@ -1,4 +1,7 @@
#pragma once
+
+#include "power_button_profile.hpp"
+
#include <sdbusplus/bus.hpp>
#include <sdbusplus/bus/match.hpp>
@@ -157,6 +160,11 @@
* @brief Matches on the ocp debug host selector button released signal
*/
std::unique_ptr<sdbusplus::bus::match_t> debugHSButtonReleased;
+
+ /**
+ * @brief The custom power handler profile object.
+ */
+ std::unique_ptr<PowerButtonProfile> powerButtonProfile;
};
} // namespace button
diff --git a/inc/host_then_chassis_poweroff.hpp b/inc/host_then_chassis_poweroff.hpp
new file mode 100644
index 0000000..96d7aee
--- /dev/null
+++ b/inc/host_then_chassis_poweroff.hpp
@@ -0,0 +1,185 @@
+#pragma once
+#include "power_button_profile.hpp"
+
+#include <sdbusplus/bus/match.hpp>
+#include <sdeventplus/event.hpp>
+#include <sdeventplus/utility/timer.hpp>
+#include <xyz/openbmc_project/State/Host/server.hpp>
+
+#include <chrono>
+
+namespace phosphor::button
+{
+
+/**
+ * @class HostThenChassisPowerOff
+ *
+ * A custom power button handler that will do the following:
+ *
+ * If power is off:
+ * - A button press will power on as long as the BMC is
+ * in the ready state.
+ *
+ * If power is on:
+ * - A button press less than 4s won't do anything.
+ * - At 4s, issue a host power off and start a 10s timer.
+ * - If the button is released within that 10s and not pressed
+ * again, continue with the host power off.
+ * - If the button is released within that 10s and also
+ * pressed again in that 10s, do a hard power (chassis)
+ * off.
+ * - If the button is pressed throughout that 10s
+ * issue a hard power off.
+ */
+class HostThenChassisPowerOff : public PowerButtonProfile
+{
+ public:
+ enum class PowerOpState
+ {
+ powerOnPress,
+ buttonNotPressed,
+ buttonPressed,
+ buttonPressedHostOffStarted,
+ buttonReleasedHostToChassisOffWindow,
+ chassisOffStarted
+ };
+
+ /**
+ * @brief Constructor
+ * @param[in] bus - The sdbusplus bus object
+ */
+ explicit HostThenChassisPowerOff(sdbusplus::bus_t& bus) :
+ PowerButtonProfile(bus), state(PowerOpState::buttonNotPressed),
+ timer(bus.get_event(),
+ std::bind(&HostThenChassisPowerOff::timerHandler, this),
+ pollInterval)
+ {
+ timer.setEnabled(false);
+ }
+
+ /**
+ * @brief Returns the name that matches the value in
+ * meson_options.txt.
+ */
+ static constexpr std::string_view getName()
+ {
+ return "host_then_chassis_poweroff";
+ }
+
+ HostThenChassisPowerOff() = delete;
+ ~HostThenChassisPowerOff() = default;
+
+ /**
+ * @brief Called when the power button is pressed.
+ */
+ virtual void pressed() override;
+
+ /**
+ * @brief Called when the power button is released.
+ *
+ * @param[in] pressTimeMS - How long the button was pressed
+ * in milliseconds.
+ */
+ virtual void released(uint64_t pressTimeMS) override;
+
+ private:
+ /**
+ * @brief Determines if the BMC is in the ready state.
+ * @return bool If the BMC is in the ready state
+ */
+ bool isBmcReady() const;
+
+ /**
+ * @brief Determines if system (chassis) is powered on.
+ *
+ * @return bool - If power is on
+ */
+ bool isPoweredOn() const;
+
+ /**
+ * @brief Requests a host state transition
+ * @param[in] transition - The transition (like On or Off)
+ */
+ void hostTransition(
+ sdbusplus::xyz::openbmc_project::State::server::Host::Transition
+ transition);
+
+ /**
+ * @brief Powers on the system
+ */
+ void powerOn();
+
+ /**
+ * @brief Requests a host power off
+ */
+ void hostPowerOff();
+
+ /**
+ * @brief Requests a chassis power off
+ */
+ void chassisPowerOff();
+
+ /**
+ * @brief The handler for the 1s timer that runs when determining
+ * how to power off.
+ *
+ * A 1 second timer is used so that there is the ability to emit
+ * a power off countdown if necessary.
+ */
+ void timerHandler();
+
+ /**
+ * @brief Sets the time the host will be powered off if the
+ * button is still pressed - 4 seconds in the future.
+ */
+ inline void setHostOffTime()
+ {
+ hostOffTime = std::chrono::steady_clock::now() + hostOffInterval;
+ }
+
+ /**
+ * @brief Sets the time the chassis will be powered off if the
+ * button is still pressed or pressed again - 10 seconds
+ * in the future.
+ */
+ inline void setChassisOffTime()
+ {
+ chassisOffTime = std::chrono::steady_clock::now() + chassisOffInterval;
+ }
+
+ /**
+ * @brief The interval the timer handler is called at.
+ */
+ static constexpr std::chrono::milliseconds pollInterval{1000};
+
+ /**
+ * @brief Default button hold down interval constant
+ */
+ static constexpr std::chrono::milliseconds hostOffInterval{4000};
+
+ /**
+ * @brief The time between a host power off and chassis power off.
+ */
+ static constexpr std::chrono::milliseconds chassisOffInterval{10000};
+
+ /**
+ * @brief The current state of the handler.
+ */
+ PowerOpState state;
+
+ /**
+ * @brief When the host will be powered off.
+ */
+ std::chrono::time_point<std::chrono::steady_clock> hostOffTime;
+
+ /**
+ * @brief When the chassis will be powered off.
+ */
+ std::chrono::time_point<std::chrono::steady_clock> chassisOffTime;
+
+ /**
+ * @brief The timer object.
+ */
+ sdeventplus::utility::Timer<sdeventplus::ClockId::Monotonic> timer;
+};
+} // namespace phosphor::button
diff --git a/inc/power_button_profile.hpp b/inc/power_button_profile.hpp
new file mode 100644
index 0000000..37bfbe6
--- /dev/null
+++ b/inc/power_button_profile.hpp
@@ -0,0 +1,72 @@
+#pragma once
+
+#include "config.h"
+
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/bus/match.hpp>
+#include <sdbusplus/message.hpp>
+
+#include <string>
+
+namespace phosphor::button
+{
+
+constexpr auto powerButtonInterface =
+ "xyz.openbmc_project.Chassis.Buttons.Power";
+namespace sdbusRule = sdbusplus::bus::match::rules;
+
+/**
+ * @class PowerButtonProfile
+ *
+ * Abstract base class for custom power button profiles.
+ *
+ * Calls a derived class's pressed() and released()
+ * functions when the power button is pressed and
+ * released.
+ */
+class PowerButtonProfile
+{
+ public:
+ PowerButtonProfile(sdbusplus::bus_t& bus) :
+ bus(bus),
+ pressedMatch(bus,
+ sdbusRule::type::signal() + sdbusRule::member("Pressed") +
+ sdbusRule::path(POWER_DBUS_OBJECT_NAME) +
+ sdbusRule::interface(powerButtonInterface),
+ std::bind(&PowerButtonProfile::pressedHandler, this,
+ std::placeholders::_1)),
+ releasedMatch(bus,
+ sdbusRule::type::signal() +
+ sdbusRule::member("Released") +
+ sdbusRule::path(POWER_DBUS_OBJECT_NAME) +
+ sdbusRule::interface(powerButtonInterface),
+ std::bind(&PowerButtonProfile::releasedHandler, this,
+ std::placeholders::_1))
+ {}
+
+ virtual ~PowerButtonProfile() = default;
+
+ void pressedHandler(sdbusplus::message_t /* msg*/)
+ {
+ pressed();
+ }
+
+ void releasedHandler(sdbusplus::message_t msg)
+ {
+ auto time = msg.unpack<uint64_t>();
+ released(time);
+ }
+
+ virtual void pressed() = 0;
+
+ virtual void released(uint64_t pressTimeMS) = 0;
+
+ protected:
+ sdbusplus::bus_t& bus;
+
+ private:
+ sdbusplus::bus::match_t pressedMatch;
+ sdbusplus::bus::match_t releasedMatch;
+};
+
+} // namespace phosphor::button
diff --git a/inc/power_button_profile_factory.hpp b/inc/power_button_profile_factory.hpp
new file mode 100644
index 0000000..de11e84
--- /dev/null
+++ b/inc/power_button_profile_factory.hpp
@@ -0,0 +1,79 @@
+#pragma once
+
+#include "config.h"
+
+#include "power_button_profile.hpp"
+
+#include <memory>
+#include <unordered_map>
+
+namespace phosphor::button
+{
+
+using powerButtonProfileCreator =
+ std::function<std::unique_ptr<PowerButtonProfile>(sdbusplus::bus_t& bus)>;
+
+/**
+ * @class PowerButtonProfileFactory
+ *
+ * Creates the custom power button profile class if one is set with
+ * the 'power-button-profile' meson option.
+ *
+ * The createProfile() method will return a nullptr if no custom
+ * profile is enabled.
+ */
+class PowerButtonProfileFactory
+{
+ public:
+ static PowerButtonProfileFactory& instance()
+ {
+ static PowerButtonProfileFactory factory;
+ return factory;
+ }
+
+ template <typename T>
+ void addToRegistry()
+ {
+ profileRegistry[std::string(T::getName())] = [](sdbusplus::bus_t& bus) {
+ return std::make_unique<T>(bus);
+ };
+ }
+
+ std::unique_ptr<PowerButtonProfile> createProfile(sdbusplus::bus_t& bus)
+ {
+ // Find the creator method named after the
+ // 'power-button-profile' option value.
+ auto objectIter = profileRegistry.find(POWER_BUTTON_PROFILE);
+ if (objectIter != profileRegistry.end())
+ {
+ return objectIter->second(bus);
+ }
+ else
+ {
+ return nullptr;
+ }
+ }
+
+ private:
+ PowerButtonProfileFactory() = default;
+
+ std::unordered_map<std::string, powerButtonProfileCreator> profileRegistry;
+};
+
+/**
+ * @brief Registers a power button profile with the factory.
+ *
+ * Declare a static instance of this at the top of the profile
+ * .cpp file like:
+ * static PowerButtonProfileRegister<MyClass> register;
+ */
+template <class T>
+class PowerButtonProfileRegister
+{
+ public:
+ PowerButtonProfileRegister()
+ {
+ PowerButtonProfileFactory::instance().addToRegistry<T>();
+ }
+};
+} // namespace phosphor::button
diff --git a/meson.build b/meson.build
index dc55722..0766a32 100644
--- a/meson.build
+++ b/meson.build
@@ -31,6 +31,8 @@
'/xyz/openbmc_project/state/host')
conf_data.set_quoted('ID_LED_GROUP', get_option('id-led-group'))
+conf_data.set_quoted('POWER_BUTTON_PROFILE', get_option('power-button-profile'))
+
conf_data.set('LONG_PRESS_TIME_MS', get_option('long-press-time-ms'))
conf_data.set('LOOKUP_GPIO_BASE', get_option('lookup-gpio-base').enabled())
@@ -78,6 +80,7 @@
sources_handler = [
'src/button_handler_main.cpp',
'src/button_handler.cpp',
+ 'src/host_then_chassis_poweroff.cpp',
]
executable(
diff --git a/meson_options.txt b/meson_options.txt
index 683a4f5..8fa5e59 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -18,3 +18,11 @@
value: 'enabled',
description : 'Look up the GPIO base value in /sys/class/gpio. Otherwise use a base of 0.'
)
+
+option(
+ 'power-button-profile',
+ type: 'combo',
+ choices: ['default', 'host_then_chassis_poweroff'],
+ value: 'default',
+ description : 'Used to select a custom power button behavior profile.'
+)
diff --git a/src/button_handler.cpp b/src/button_handler.cpp
index f38882e..801cc9e 100755
--- a/src/button_handler.cpp
+++ b/src/button_handler.cpp
@@ -2,6 +2,8 @@
#include "button_handler.hpp"
+#include "power_button_profile_factory.hpp"
+
#include <phosphor-logging/lg2.hpp>
#include <xyz/openbmc_project/State/Chassis/server.hpp>
#include <xyz/openbmc_project/State/Host/server.hpp>
@@ -39,13 +41,21 @@
if (!getService(POWER_DBUS_OBJECT_NAME, powerButtonIface).empty())
{
lg2::info("Starting power button handler");
- powerButtonReleased = std::make_unique<sdbusplus::bus::match_t>(
- bus,
- sdbusRule::type::signal() + sdbusRule::member("Released") +
- sdbusRule::path(POWER_DBUS_OBJECT_NAME) +
- sdbusRule::interface(powerButtonIface),
- std::bind(std::mem_fn(&Handler::powerReleased), this,
- std::placeholders::_1));
+
+ // Check for a custom handler
+ powerButtonProfile =
+ PowerButtonProfileFactory::instance().createProfile(bus);
+
+ if (!powerButtonProfile)
+ {
+ powerButtonReleased = std::make_unique<sdbusplus::bus::match_t>(
+ bus,
+ sdbusRule::type::signal() + sdbusRule::member("Released") +
+ sdbusRule::path(POWER_DBUS_OBJECT_NAME) +
+ sdbusRule::interface(powerButtonIface),
+ std::bind(std::mem_fn(&Handler::powerReleased), this,
+ std::placeholders::_1));
+ }
}
}
catch (const sdbusplus::exception_t& e)
diff --git a/src/host_then_chassis_poweroff.cpp b/src/host_then_chassis_poweroff.cpp
new file mode 100644
index 0000000..0178983
--- /dev/null
+++ b/src/host_then_chassis_poweroff.cpp
@@ -0,0 +1,237 @@
+#include "config.h"
+
+#include "host_then_chassis_poweroff.hpp"
+
+#include "power_button_profile_factory.hpp"
+
+#include <phosphor-logging/lg2.hpp>
+#include <xyz/openbmc_project/State/BMC/server.hpp>
+#include <xyz/openbmc_project/State/Chassis/server.hpp>
+
+namespace phosphor::button
+{
+
+// Register the profile with the factory.
+static PowerButtonProfileRegister<HostThenChassisPowerOff> profileRegister;
+
+namespace service
+{
+constexpr auto bmcState = "xyz.openbmc_project.State.BMC";
+constexpr auto chassisState = "xyz.openbmc_project.State.Chassis";
+constexpr auto hostState = "xyz.openbmc_project.State.Host";
+} // namespace service
+
+namespace object_path
+{
+constexpr auto bmcState = "/xyz/openbmc_project/state/bmc0";
+constexpr auto chassisState = "/xyz/openbmc_project/state/chassis0";
+constexpr auto hostState = "/xyz/openbmc_project/state/host0";
+} // namespace object_path
+
+namespace interface
+{
+constexpr auto property = "org.freedesktop.DBus.Properties";
+constexpr auto bmcState = "xyz.openbmc_project.State.BMC";
+constexpr auto chassisState = "xyz.openbmc_project.State.Chassis";
+constexpr auto hostState = "xyz.openbmc_project.State.Host";
+} // namespace interface
+
+using namespace sdbusplus::xyz::openbmc_project::State::server;
+
+void HostThenChassisPowerOff::pressed()
+{
+ lg2::info("Power button pressed");
+
+ try
+ {
+ // If power not on - power on
+ if (!isPoweredOn())
+ {
+ if (!isBmcReady())
+ {
+ lg2::error("BMC not at ready state yet, cannot power on");
+ return;
+ }
+
+ state = PowerOpState::powerOnPress;
+ powerOn();
+ return;
+ }
+ }
+ catch (const sdbusplus::exception_t& e)
+ {
+ lg2::error(
+ "Exception while processing power button press. Cannot continue");
+ return;
+ }
+
+ // Power is on ...
+
+ if (state == PowerOpState::buttonNotPressed)
+ {
+ lg2::info("Starting countdown to power off");
+ state = PowerOpState::buttonPressed;
+ setHostOffTime();
+ timer.restart(pollInterval);
+ }
+
+ // Button press during host off to chassis off window.
+ // Causes a chassis power off.
+ else if (state == PowerOpState::buttonReleasedHostToChassisOffWindow)
+ {
+ lg2::info("Starting chassis power off due to button press");
+ state = PowerOpState::chassisOffStarted;
+ timer.setEnabled(false);
+ chassisPowerOff();
+ }
+}
+
+void HostThenChassisPowerOff::released(uint64_t /*pressTimeMS*/)
+{
+ lg2::info("Power button released");
+
+ // Button released in the host to chassis off window.
+ // Timer continues to run in case button is pressed again
+ // in the window.
+ if (state == PowerOpState::buttonPressedHostOffStarted)
+ {
+ state = PowerOpState::buttonReleasedHostToChassisOffWindow;
+ return;
+ }
+
+ state = PowerOpState::buttonNotPressed;
+ timer.setEnabled(false);
+}
+
+void HostThenChassisPowerOff::timerHandler()
+{
+ const auto now = std::chrono::steady_clock::now();
+
+ if ((state == PowerOpState::buttonPressed) && (now >= hostOffTime))
+ {
+ // Start the host power off and start the chassis
+ // power off countdown.
+ state = PowerOpState::buttonPressedHostOffStarted;
+ setChassisOffTime();
+ hostPowerOff();
+ }
+ else if ((state == PowerOpState::buttonPressedHostOffStarted) &&
+ (now >= chassisOffTime))
+ {
+ // Button still pressed and it passed the chassis off time.
+ // Issue the chassis power off.
+ state = PowerOpState::chassisOffStarted;
+ timer.setEnabled(false);
+ chassisPowerOff();
+ }
+}
+
+void HostThenChassisPowerOff::hostTransition(Host::Transition transition)
+{
+ try
+ {
+ std::variant<std::string> state = convertForMessage(transition);
+
+ lg2::info("Power button action requesting host transition of {TRANS}",
+ "TRANS", std::get<std::string>(state));
+
+ auto method = bus.new_method_call(service::hostState,
+ object_path::hostState,
+ interface::property, "Set");
+ method.append(interface::hostState, "RequestedHostTransition", state);
+
+ bus.call(method);
+ }
+ catch (const sdbusplus::exception_t& e)
+ {
+ lg2::error("Failed requesting host transition {TRANS}: {ERROR}",
+ "TRANS", convertForMessage(transition), "ERROR", e);
+ }
+}
+
+void HostThenChassisPowerOff::powerOn()
+{
+ hostTransition(Host::Transition::On);
+}
+
+void HostThenChassisPowerOff::hostPowerOff()
+{
+ hostTransition(Host::Transition::Off);
+}
+
+void HostThenChassisPowerOff::chassisPowerOff()
+{
+ lg2::info("Power button action requesting chassis power off");
+
+ try
+ {
+ std::variant<std::string> state =
+ convertForMessage(Chassis::Transition::Off);
+
+ auto method = bus.new_method_call(service::chassisState,
+ object_path::chassisState,
+ interface::property, "Set");
+ method.append(interface::chassisState, "RequestedPowerTransition",
+ state);
+
+ bus.call(method);
+ }
+ catch (const sdbusplus::exception_t& e)
+ {
+ lg2::error("Failed requesting chassis off: {ERROR}", "ERROR", e);
+ }
+}
+
+bool HostThenChassisPowerOff::isPoweredOn() const
+{
+ Chassis::PowerState chassisState;
+
+ try
+ {
+ auto method = bus.new_method_call(service::chassisState,
+ object_path::chassisState,
+ interface::property, "Get");
+ method.append(interface::chassisState, "CurrentPowerState");
+ auto result = bus.call(method);
+
+ std::variant<std::string> state;
+ result.read(state);
+
+ chassisState =
+ Chassis::convertPowerStateFromString(std::get<std::string>(state));
+ }
+ catch (const sdbusplus::exception_t& e)
+ {
+ lg2::error("Failed checking if chassis is on: {ERROR}", "ERROR", e);
+ throw;
+ }
+
+ return chassisState == Chassis::PowerState::On;
+}
+
+bool HostThenChassisPowerOff::isBmcReady() const
+{
+ BMC::BMCState bmcState;
+
+ try
+ {
+ auto method = bus.new_method_call(service::bmcState,
+ object_path::bmcState,
+ interface::property, "Get");
+ method.append(interface::bmcState, "CurrentBMCState");
+ auto result = bus.call(method);
+
+ std::variant<std::string> state;
+ result.read(state);
+
+ bmcState = BMC::convertBMCStateFromString(std::get<std::string>(state));
+ }
+ catch (const sdbusplus::exception_t& e)
+ {
+ lg2::error("Failed reading BMC state interface: {}", "ERROR", e);
+ throw;
+ }
+
+ return bmcState == BMC::BMCState::Ready;
+}
+} // namespace phosphor::button