blob: 5c842aa415a0920605db58f1cfd78ee878da55e0 [file] [log] [blame]
// Copyright 2024 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#pragma once
#include <optional>
#include <string_view>
#include <utility>
#include "modules/air_sensor/air_sensor.h"
#include "modules/edge_detector/hysteresis_edge_detector.h"
#include "modules/led/polychrome_led.h"
#include "modules/morse_code/encoder.h"
#include "modules/pubsub/pubsub_events.h"
#include "modules/state_manager/common_base_union.h"
#include "pw_string/string.h"
namespace sense {
// Wraps a PolychromeLed and sets brightness from ambient light readings.
class AmbientLightAdjustedLed {
public:
static constexpr uint8_t kMinBrightness = 10;
static constexpr uint8_t kDefaultBrightness = 160;
static constexpr uint8_t kMaxBrightness = 255;
AmbientLightAdjustedLed(PolychromeLed& led);
void SetColor(const LedValue& color) {
led_.SetColor(color.r(), color.g(), color.b());
}
void SetOnOff(bool turn_on) { led_.SetOnOff(turn_on); }
// Recalculates the brightness level when the ambient light changes.
void UpdateBrightnessFromAmbientLight(float ambient_light_sample_lux);
private:
void UpdateAverageAmbientLight(float ambient_light_sample_lux);
PolychromeLed& led_;
std::optional<float> ambient_light_lux_;
};
// Manages state for the "production" Sense app.
//
// This class is NOT thread safe. Must only be interacted with from the PubSub
// thread.
class StateManager {
public:
using TimerToken = ::pw::tokenizer::Token;
static constexpr TimerToken kRepeatAlarmToken =
PW_TOKENIZE_STRING("repeat alarm");
static constexpr uint16_t kRepeatAlarmTimeout = 1;
static constexpr TimerToken kSilenceAlarmToken =
PW_TOKENIZE_STRING("re-enable alarm");
static constexpr uint16_t kSilenceAlarmTimeout = 60;
static constexpr TimerToken kThresholdModeToken =
PW_TOKENIZE_STRING("exit threshold mode");
static constexpr uint16_t kThresholdModeTimeout = 3;
static constexpr uint16_t kDefaultThreshold =
static_cast<uint16_t>(AirSensor::Score::kYellow);
static constexpr uint16_t kThresholdIncrement =
static_cast<uint16_t>(AirSensor::Score::kOrange) -
static_cast<uint16_t>(AirSensor::Score::kRed);
static constexpr uint16_t kMaxThreshold =
static_cast<uint16_t>(AirSensor::Score::kCyan);
StateManager(PubSub& pubsub, PolychromeLed& led);
StateManager(const StateManager&) = delete;
StateManager& operator=(const StateManager&) = delete;
static const char* AirQualityDescription(uint16_t score);
private:
static constexpr size_t kMaxMorseCodeStringLen = 16;
static_assert(kMaxMorseCodeStringLen <= Encoder::kMaxMsgLen);
using MorseCodeString = ::pw::InlineString<kMaxMorseCodeStringLen>;
// Represents a state in the Sense app state machine.
class State {
public:
State(StateManager& manager, const char* name)
: manager_(manager), name_(name) {}
virtual ~State() = default;
// Name of the state for logging.
const char* name() const { return name_; }
/// Button A enters `ThresholdMode` by default.
virtual void ButtonAPressed() { manager_.SetState<ThresholdMode>(); }
/// Button B enters `ThresholdMode` by default.
virtual void ButtonBPressed() { manager_.SetState<ThresholdMode>(); }
/// Button X enters resets the mode to either `MonitorMode` or `AlarmMode`
/// by default, depending on the current air quality..
virtual void ButtonXPressed() { manager_.ResetMode(); }
/// Button Y enters `MorseReadoutMode` by default.
virtual void ButtonYPressed() { manager().SetState<MorseReadoutMode>(); }
// Update the LED color by default.
virtual void OnLedValue(const LedValue& value) {
manager().led_.SetColor(value);
}
// Ignore Morse code edges by default.
virtual void OnMorseCodeValue(const MorseCodeValue&) {}
// Handles re-enabling alarms that were previously silenced.
virtual void OnTimerExpired(const TimerExpired& timer) {
if (timer.token == kSilenceAlarmToken) {
manager().alarm_silenced_ = false;
}
}
protected:
StateManager& manager() { return manager_; }
private:
StateManager& manager_;
const char* name_;
};
/// Mode for monitoring the air quality.
///
/// Inherits default button mapping.
class MonitorMode final : public State {
public:
MonitorMode(StateManager& manager) : State(manager, "MonitorMode") {}
};
/// Mode for displaying and modifying the the air quality alarm threshold.
///
/// Inherits default button mapping, except:
/// * Button A increments the threshold.
/// * Button B decrements the threshold.
///
/// The mode will timeout and return to the default mode after 3 seconds of no
/// button being pressed.
class ThresholdMode final : public State {
public:
ThresholdMode(StateManager& manager) : State(manager, "ThresholdMode") {
manager.DisplayThreshold();
}
void ButtonAPressed() override { manager().IncrementThreshold(); }
void ButtonBPressed() override { manager().DecrementThreshold(); }
void ButtonXPressed() override { manager().ResetMode(); }
void ButtonYPressed() override { manager().ResetMode(); }
void OnLedValue(const LedValue&) override {}
void OnTimerExpired(const TimerExpired& timer) override {
if (timer.token == kThresholdModeToken) {
// Blink three times before returning to the default mode.
manager().SetState<MorseReadoutMode>("TTT");
} else {
State::OnTimerExpired(timer);
}
}
};
/// Mode representing a triggered air quality alarm.
///
/// Inherits default button mapping, except:
/// * Button X silences the alarm for 60 seconds.
/// * Button Y does nothing.
class AlarmMode final : public State {
public:
AlarmMode(StateManager& manager) : State(manager, "AlarmMode") {
manager.FormatAirQuality(msg_);
manager.StartMorseReadout(msg_);
}
// Since morse code leaves the LED off, turn it back on.
~AlarmMode() { manager().led_.SetOnOff(true); }
void ButtonXPressed() override { manager().SilenceAlarms(); }
void ButtonYPressed() override {}
void OnMorseCodeValue(const MorseCodeValue& value) override {
manager().led_.SetOnOff(value.turn_on);
if (value.message_finished) {
manager().RepeatAlarm();
}
}
void OnTimerExpired(const TimerExpired& timer) override {
if (timer.token == kRepeatAlarmToken) {
manager().FormatAirQuality(msg_);
manager().StartMorseReadout(msg_);
} else {
State::OnTimerExpired(timer);
}
}
private:
MorseCodeString msg_;
};
/// Mode that displays the current air quality in Morse code.
///
/// Inherits default button mapping, except:
/// * Button Y restarts the air quality display.
class MorseReadoutMode final : public State {
public:
MorseReadoutMode(StateManager& manager)
: State(manager, "MorseReadoutMode") {
manager.FormatAirQuality(msg_);
manager.StartMorseReadout(msg_);
}
MorseReadoutMode(StateManager& manager, std::string_view msg)
: State(manager, "MorseReadoutMode"), msg_(msg) {
manager.StartMorseReadout(msg_);
}
// Since morse code leaves the LED off, turn it back on.
~MorseReadoutMode() { manager().led_.SetOnOff(true); }
// Keep the current color.
void OnLedValue(const LedValue&) override {}
void OnMorseCodeValue(const MorseCodeValue& value) override {
manager().led_.SetOnOff(value.turn_on);
if (value.message_finished) {
manager().ResetMode();
}
}
private:
MorseCodeString msg_;
};
/// Responds to a PubSub event.
void Update(Event event);
template <typename StateType, typename... Args>
void SetState(Args&&... args) {
const char* old_state = state_.get().name();
state_.emplace<StateType>(*this, std::forward<Args>(args)...);
BroadcastState();
LogStateChange(old_state);
}
/// Sets the state to `MonitorMode` or `AlarmMode`, depending on the current
/// air quality.
void ResetMode();
/// Increases the current alarm threshold.
void IncrementThreshold();
/// Decreases the current alarm threshold.
void DecrementThreshold();
/// Suppresses `AlarmMode` for 60 seconds.
void SilenceAlarms();
/// Sets the LED to reflect the current alarm threshold.
void DisplayThreshold();
/// Sets the current alarm threshold.
void SetAlarmThreshold(uint16_t alarm_threshold);
/// Incorporates a new air quality reading from the air sensor, changing the
/// LED color and triggering alarms as appropriate.
void UpdateAirQuality(uint16_t score);
/// Send a timer request to repeat an alarm.
void RepeatAlarm();
/// Sends a request to the Morse encoder to send `OnMorseCodeValue` events for
/// the given message.
void StartMorseReadout(std::string_view msg);
/// Sends a request to the Morse encoder to send `OnMorseCodeValue` events for
/// a description of the current air quality.
void StartMorseReadout();
/// Sets the given string to a representation of the current air quality.
void FormatAirQuality(MorseCodeString& msg);
void LogStateChange(const char* old_state) const;
void BroadcastState() const;
void HandleControlEvent(StateManagerControl& event);
constexpr uint16_t air_quality() const {
return air_quality_.value_or(AirSensor::kMaxScore + 1);
}
std::optional<uint16_t> air_quality_;
bool alarm_ = false;
bool alarm_silenced_ = false;
uint16_t alarm_threshold_ = kDefaultThreshold;
HysteresisEdgeDetector<uint16_t> edge_detector_;
PubSub& pubsub_;
AmbientLightAdjustedLed led_;
CommonBaseUnion<State,
MonitorMode,
ThresholdMode,
AlarmMode,
MorseReadoutMode>
state_;
};
} // namespace sense