modules/state_manager: Add RPC service
Creates an RPC service to query the current state and update air quality
alarm thresholds.
Change-Id: Ifdf5d89dcbf175680d72501af8718a97f7c82b00
Reviewed-on: https://pigweed-internal-review.git.corp.google.com/c/pigweed/showcase/rp2/+/73888
Commit-Queue: Alexei Frolov <frolv@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/apps/production/BUILD.bazel b/apps/production/BUILD.bazel
index 8ca7488..3b246fb 100644
--- a/apps/production/BUILD.bazel
+++ b/apps/production/BUILD.bazel
@@ -28,10 +28,11 @@
"//modules/board:service",
"//modules/event_timers",
"//modules/morse_code:encoder",
+ "//modules/proximity:manager",
"//modules/pubsub:service",
"//modules/state_manager",
+ "//modules/state_manager:service",
"//system:pubsub",
- "//modules/proximity:manager",
"//system:worker",
"//system",
":threads",
diff --git a/apps/production/main.cc b/apps/production/main.cc
index 1f76b39..dbb9669 100644
--- a/apps/production/main.cc
+++ b/apps/production/main.cc
@@ -22,6 +22,7 @@
#include "modules/proximity/manager.h"
#include "modules/pubsub/service.h"
#include "modules/sampling_thread/sampling_thread.h"
+#include "modules/state_manager/service.h"
#include "modules/state_manager/state_manager.h"
#include "pw_assert/check.h"
#include "pw_log/log.h"
@@ -36,6 +37,8 @@
void InitStateManager() {
static StateManager state_manager(system::PubSub(), system::PolychromeLed());
+ static StateManagerService state_manager_service(system::PubSub());
+ pw::System().rpc_server().RegisterService(state_manager_service);
}
void InitEventTimers() {
diff --git a/modules/pubsub/BUILD.bazel b/modules/pubsub/BUILD.bazel
index b31c796..692c43c 100644
--- a/modules/pubsub/BUILD.bazel
+++ b/modules/pubsub/BUILD.bazel
@@ -62,10 +62,12 @@
hdrs = ["service.h"],
implementation_deps = [
"@pigweed//pw_log",
+ "@pigweed//pw_string",
],
deps = [
":events",
":nanopb_rpc",
+ "//modules/state_manager",
],
)
@@ -91,6 +93,7 @@
strip_import_prefix = "/modules/pubsub",
deps = [
"//modules/morse_code:proto",
+ "//modules/state_manager:proto",
"@pigweed//pw_protobuf:common_proto",
],
)
diff --git a/modules/pubsub/pubsub.proto b/modules/pubsub/pubsub.proto
index dc8adb7..9e0bf2e 100644
--- a/modules/pubsub/pubsub.proto
+++ b/modules/pubsub/pubsub.proto
@@ -17,6 +17,7 @@
import "pw_protobuf_protos/common.proto";
import "morse_code.proto";
+import "state_manager.proto";
service PubSub {
rpc Publish(Event) returns (pw.protobuf.Empty);
@@ -48,6 +49,16 @@
uint32 token = 1;
}
+message StateManagerControl {
+ enum Action {
+ UNKNOWN = 0;
+ INCREMENT_THRESHOLD = 1;
+ DECREMENT_THRESHOLD = 2;
+ SILENCE_ALARMS = 3;
+ }
+ Action action = 1;
+}
+
message Event {
// This definition must be kept up to date with
// modules/pubsub/pubsub_events.h.
@@ -64,5 +75,7 @@
TimerExpired timer_expired = 10;
morse_code.SendRequest morse_encode_request = 11;
float ambient_light_lux = 12;
+ state_manager.State sense_state = 13;
+ StateManagerControl state_manager_control = 14;
}
}
diff --git a/modules/pubsub/pubsub_events.h b/modules/pubsub/pubsub_events.h
index 404bddf..eb3bb7c 100644
--- a/modules/pubsub/pubsub_events.h
+++ b/modules/pubsub/pubsub_events.h
@@ -110,6 +110,23 @@
bool message_finished;
};
+struct SenseState {
+ bool alarm;
+ uint16_t alarm_threshold;
+ uint16_t air_quality;
+ const char* air_quality_description;
+};
+
+struct StateManagerControl {
+ enum Action {
+ kIncrementThreshold,
+ kDecrementThreshold,
+ kSilenceAlarms,
+ } action;
+
+ explicit constexpr StateManagerControl(Action a) : action(a) {}
+};
+
// This definition must be kept up to date with modules/pubsub/pubsub.proto and
// the EventType enum.
using Event = std::variant<ButtonA,
@@ -123,7 +140,9 @@
AmbientLightSample,
AirQuality,
MorseEncodeRequest,
- MorseCodeValue>;
+ MorseCodeValue,
+ SenseState,
+ StateManagerControl>;
// Index versions of Event variants, to support finding the event
enum EventType : size_t {
@@ -139,7 +158,9 @@
kAirQuality,
kMorseEncodeRequest,
kMorseCodeValue,
- kLastEventType = kMorseCodeValue,
+ kSenseState,
+ kStateManagerControl,
+ kLastEventType = kStateManagerControl,
};
static_assert(kLastEventType + 1 == std::variant_size_v<Event>,
diff --git a/modules/pubsub/service.cc b/modules/pubsub/service.cc
index 8d992c0..0596e4e 100644
--- a/modules/pubsub/service.cc
+++ b/modules/pubsub/service.cc
@@ -15,7 +15,9 @@
#include "modules/pubsub/service.h"
+#include "modules/state_manager/state_manager.h"
#include "pw_log/log.h"
+#include "pw_string/util.h"
namespace sense {
namespace {
@@ -69,6 +71,31 @@
const auto& morse = std::get<MorseCodeValue>(event);
proto.type.morse_code_value.turn_on = morse.turn_on;
proto.type.morse_code_value.message_finished = morse.message_finished;
+ } else if (std::holds_alternative<SenseState>(event)) {
+ proto.which_type = pubsub_Event_sense_state_tag;
+ const auto& state = std::get<SenseState>(event);
+ proto.type.sense_state.alarm_active = state.alarm;
+ proto.type.sense_state.alarm_threshold = state.alarm_threshold;
+ proto.type.sense_state.aq_score = state.air_quality;
+ pw::string::Copy(state.air_quality_description,
+ proto.type.sense_state.aq_description);
+ } else if (std::holds_alternative<StateManagerControl>(event)) {
+ proto.which_type = pubsub_Event_state_manager_control_tag;
+ const auto& control = std::get<StateManagerControl>(event);
+ switch (control.action) {
+ case StateManagerControl::kDecrementThreshold:
+ proto.type.state_manager_control.action =
+ pubsub_StateManagerControl_Action_DECREMENT_THRESHOLD;
+ break;
+ case StateManagerControl::kIncrementThreshold:
+ proto.type.state_manager_control.action =
+ pubsub_StateManagerControl_Action_INCREMENT_THRESHOLD;
+ break;
+ case StateManagerControl::kSilenceAlarms:
+ proto.type.state_manager_control.action =
+ pubsub_StateManagerControl_Action_SILENCE_ALARMS;
+ break;
+ }
} else {
PW_LOG_WARN("Unimplemented pubsub service event");
}
@@ -104,6 +131,31 @@
return ProximityStateChange{.proximity = proto.type.proximity};
case pubsub_Event_air_quality_tag:
return AirQuality{.score = static_cast<uint16_t>(proto.type.air_quality)};
+ case pubsub_Event_sense_state_tag:
+ return SenseState{
+ .alarm = proto.type.sense_state.alarm_active,
+ .alarm_threshold =
+ static_cast<uint16_t>(proto.type.sense_state.alarm_threshold),
+ .air_quality = static_cast<uint16_t>(proto.type.sense_state.aq_score),
+ .air_quality_description = StateManager::AirQualityDescription(
+ proto.type.sense_state.aq_score),
+ };
+ case pubsub_Event_state_manager_control_tag:
+ StateManagerControl::Action action;
+ switch (proto.type.state_manager_control.action) {
+ case pubsub_StateManagerControl_Action_DECREMENT_THRESHOLD:
+ action = StateManagerControl::kDecrementThreshold;
+ break;
+ case pubsub_StateManagerControl_Action_INCREMENT_THRESHOLD:
+ action = StateManagerControl::kIncrementThreshold;
+ break;
+ case pubsub_StateManagerControl_Action_SILENCE_ALARMS:
+ action = StateManagerControl::kSilenceAlarms;
+ break;
+ case pubsub_StateManagerControl_Action_UNKNOWN:
+ return pw::Status::InvalidArgument();
+ }
+ return StateManagerControl(action);
default:
return pw::Status::Unimplemented();
}
diff --git a/modules/state_manager/BUILD.bazel b/modules/state_manager/BUILD.bazel
index f3f1ce4..ef37a6d 100644
--- a/modules/state_manager/BUILD.bazel
+++ b/modules/state_manager/BUILD.bazel
@@ -12,6 +12,14 @@
# License for the specific language governing permissions and limitations under
# the License.
+load(
+ "@pigweed//pw_protobuf_compiler:pw_proto_library.bzl",
+ "nanopb_proto_library",
+ "nanopb_rpc_proto_library",
+ "pw_proto_filegroup",
+)
+load("@rules_python//python:proto.bzl", "py_proto_library")
+
package(default_visibility = ["//visibility:public"])
cc_library(
@@ -40,3 +48,47 @@
hdrs = ["common_base_union.h"],
visibility = ["//visibility:private"],
)
+
+cc_library(
+ name = "service",
+ srcs = ["service.cc"],
+ hdrs = ["service.h"],
+ deps = [
+ ":nanopb_rpc",
+ "//modules/pubsub:events",
+ "@pigweed//pw_string",
+ "@pigweed//pw_sync:interrupt_spin_lock",
+ "@pigweed//pw_sync:lock_annotations",
+ ],
+)
+
+pw_proto_filegroup(
+ name = "proto_and_options",
+ srcs = ["state_manager.proto"],
+ options_files = ["state_manager.options"],
+)
+
+proto_library(
+ name = "proto",
+ srcs = [":proto_and_options"],
+ strip_import_prefix = "/modules/state_manager",
+ deps = [
+ "@pigweed//pw_protobuf:common_proto",
+ ],
+)
+
+nanopb_proto_library(
+ name = "nanopb",
+ deps = [":proto"],
+)
+
+nanopb_rpc_proto_library(
+ name = "nanopb_rpc",
+ nanopb_proto_library_deps = [":nanopb"],
+ deps = [":proto"],
+)
+
+py_proto_library(
+ name = "py_pb2",
+ deps = [":proto"],
+)
diff --git a/modules/state_manager/service.cc b/modules/state_manager/service.cc
new file mode 100644
index 0000000..fecc2e6
--- /dev/null
+++ b/modules/state_manager/service.cc
@@ -0,0 +1,69 @@
+// 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.
+
+#include "modules/state_manager/service.h"
+
+#include <mutex>
+
+#include "pw_string/util.h"
+
+namespace sense {
+
+StateManagerService::StateManagerService(PubSub& pubsub) : pubsub_(&pubsub) {
+ pubsub_->SubscribeTo<SenseState>([this](SenseState event) {
+ std::lock_guard lock(current_state_lock_);
+ current_state_ = event;
+ });
+}
+
+pw::Status StateManagerService::ChangeThreshold(
+ const state_manager_ChangeThresholdRequest& request, pw_protobuf_Empty&) {
+ bool success;
+
+ if (request.increment) {
+ success = pubsub_->Publish(
+ StateManagerControl(StateManagerControl::kIncrementThreshold));
+ } else {
+ success = pubsub_->Publish(
+ StateManagerControl(StateManagerControl::kDecrementThreshold));
+ }
+
+ return success ? pw::OkStatus() : pw::Status::Unavailable();
+}
+
+pw::Status StateManagerService::SilenceAlarm(const pw_protobuf_Empty&,
+ pw_protobuf_Empty&) {
+ bool success = pubsub_->Publish(
+ StateManagerControl(StateManagerControl::kSilenceAlarms));
+ return success ? pw::OkStatus() : pw::Status::Unavailable();
+}
+
+pw::Status StateManagerService::GetState(const pw_protobuf_Empty&,
+ state_manager_State& response) {
+ std::lock_guard lock(current_state_lock_);
+
+ if (!current_state_.has_value()) {
+ return pw::Status::Unavailable();
+ }
+
+ const auto& current_state = current_state_.value();
+ response.alarm_active = current_state.alarm;
+ response.alarm_threshold = current_state.alarm_threshold;
+ response.aq_score = current_state.air_quality;
+ return pw::string::Copy(current_state.air_quality_description,
+ response.aq_description)
+ .status();
+}
+
+} // namespace sense
diff --git a/modules/state_manager/service.h b/modules/state_manager/service.h
new file mode 100644
index 0000000..6cca27b
--- /dev/null
+++ b/modules/state_manager/service.h
@@ -0,0 +1,43 @@
+// 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 "modules/pubsub/pubsub_events.h"
+#include "modules/state_manager/state_manager.rpc.pb.h"
+#include "pw_sync/interrupt_spin_lock.h"
+#include "pw_sync/lock_annotations.h"
+
+namespace sense {
+
+class StateManagerService final
+ : public ::state_manager::pw_rpc::nanopb::StateManager::Service<
+ StateManagerService> {
+ public:
+ StateManagerService(PubSub& pubsub);
+
+ pw::Status ChangeThreshold(
+ const state_manager_ChangeThresholdRequest& request,
+ pw_protobuf_Empty& response);
+ pw::Status SilenceAlarm(const pw_protobuf_Empty&, pw_protobuf_Empty&);
+ pw::Status GetState(const pw_protobuf_Empty&, state_manager_State& response);
+
+ private:
+ PubSub* pubsub_;
+ pw::sync::InterruptSpinLock current_state_lock_;
+ std::optional<SenseState> current_state_ PW_GUARDED_BY(current_state_lock_);
+};
+
+} // namespace sense
diff --git a/modules/state_manager/state_manager.cc b/modules/state_manager/state_manager.cc
index eddc5d4..efe1e42 100644
--- a/modules/state_manager/state_manager.cc
+++ b/modules/state_manager/state_manager.cc
@@ -77,10 +77,14 @@
led_.UpdateBrightnessFromAmbientLight(
std::get<AmbientLightSample>(event).sample_lux);
break;
+ case kStateManagerControl:
+ HandleControlEvent(std::get<StateManagerControl>(event));
+ break;
case kTimerRequest:
case kMorseEncodeRequest:
case kProximitySample:
case kProximityStateChange:
+ case kSenseState:
break; // ignore these events
}
}
@@ -122,12 +126,15 @@
PW_LOG_INFO("Air quality thresholds set: alarm at %u, silence at %u",
alarm_threshold_,
silence_threshold);
+
+ BroadcastState();
}
void StateManager::UpdateAirQuality(uint16_t score) {
AddAndSmoothExponentially(air_quality_, score);
state_.get().OnLedValue(AirSensor::GetLedValue(*air_quality_));
if (alarm_silenced_) {
+ BroadcastState();
return;
}
switch (edge_detector_.Update(*air_quality_)) {
@@ -141,6 +148,7 @@
return;
}
ResetMode();
+ BroadcastState();
}
void StateManager::RepeatAlarm() {
@@ -159,6 +167,7 @@
.timeout_s = kSilenceAlarmTimeout,
});
ResetMode();
+ BroadcastState();
}
void StateManager::ResetMode() {
@@ -173,7 +182,7 @@
pubsub_.Publish(MorseEncodeRequest{.message = msg, .repeat = 1u});
}
-static constexpr const char* AirQualityDescription(uint16_t score) {
+const char* StateManager::AirQualityDescription(uint16_t score) {
if (score > AirSensor::kMaxScore) {
return "INVALID";
}
@@ -202,7 +211,7 @@
}
void StateManager::FormatAirQuality(MorseCodeString& msg) {
- uint16_t score = air_quality_.value_or(AirSensor::kMaxScore + 1);
+ uint16_t score = air_quality();
pw::Status status = pw::string::FormatOverwrite(
msg, "AQ %s %hu", AirQualityDescription(score), score);
PW_LOG_INFO("%s", msg.data());
@@ -249,4 +258,27 @@
PW_LOG_INFO("StateManager: %s -> %s", old_state, state_.get().name());
}
+void StateManager::BroadcastState() const {
+ pubsub_.Publish(SenseState{
+ .alarm = alarm_,
+ .alarm_threshold = alarm_threshold_,
+ .air_quality = air_quality(),
+ .air_quality_description = AirQualityDescription(air_quality()),
+ });
+}
+
+void StateManager::HandleControlEvent(StateManagerControl& event) {
+ switch (event.action) {
+ case StateManagerControl::kIncrementThreshold:
+ IncrementThreshold();
+ break;
+ case StateManagerControl::kDecrementThreshold:
+ DecrementThreshold();
+ break;
+ case StateManagerControl::kSilenceAlarms:
+ SilenceAlarms();
+ break;
+ }
+}
+
} // namespace sense
diff --git a/modules/state_manager/state_manager.h b/modules/state_manager/state_manager.h
index 82fa58c..cf4874b 100644
--- a/modules/state_manager/state_manager.h
+++ b/modules/state_manager/state_manager.h
@@ -80,6 +80,8 @@
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);
@@ -254,6 +256,7 @@
void SetState(Args&&... args) {
const char* old_state = state_.get().name();
state_.emplace<StateType>(*this, std::forward<Args>(args)...);
+ BroadcastState();
LogStateChange(old_state);
}
@@ -261,15 +264,18 @@
/// air quality.
void ResetMode();
- /// Sets the LED to reflect the current alarm threshold.
- void DisplayThreshold();
-
/// 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);
@@ -280,9 +286,6 @@
/// Send a timer request to repeat an alarm.
void RepeatAlarm();
- /// Suppresses `AlarmMode` for 60 seconds.
- void SilenceAlarms();
-
/// Sends a request to the Morse encoder to send `OnMorseCodeValue` events for
/// the given message.
void StartMorseReadout(std::string_view msg);
@@ -296,6 +299,13 @@
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;
diff --git a/modules/state_manager/state_manager.options b/modules/state_manager/state_manager.options
new file mode 100644
index 0000000..b6be60a
--- /dev/null
+++ b/modules/state_manager/state_manager.options
@@ -0,0 +1 @@
+state_manager.State.aq_description max_size:32
\ No newline at end of file
diff --git a/modules/state_manager/state_manager.proto b/modules/state_manager/state_manager.proto
new file mode 100644
index 0000000..1c829fd
--- /dev/null
+++ b/modules/state_manager/state_manager.proto
@@ -0,0 +1,37 @@
+// 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.
+syntax = "proto3";
+
+package state_manager;
+
+import "pw_protobuf_protos/common.proto";
+
+service StateManager {
+ rpc ChangeThreshold(ChangeThresholdRequest) returns (pw.protobuf.Empty);
+ rpc SilenceAlarm(pw.protobuf.Empty) returns (pw.protobuf.Empty);
+ rpc GetState(pw.protobuf.Empty) returns (State);
+}
+
+message ChangeThresholdRequest {
+ // If true, increments the alarm threshold to its next step.
+ // If false, decrements.
+ bool increment = 1;
+}
+
+message State {
+ bool alarm_active = 1;
+ uint32 alarm_threshold = 2;
+ uint32 aq_score = 3;
+ string aq_description = 4;
+}
\ No newline at end of file
diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel
index ecdff4e..d07fa69 100644
--- a/tools/BUILD.bazel
+++ b/tools/BUILD.bazel
@@ -33,6 +33,7 @@
"//modules/board:py_pb2",
"//modules/morse_code:py_pb2",
"//modules/pubsub:py_pb2",
+ "//modules/state_manager:py_pb2",
"@pigweed//pw_protobuf:common_py_pb2",
"@pigweed//pw_rpc:echo_py_pb2",
"@pigweed//pw_system/py:pw_system_lib",
diff --git a/tools/sense/device.py b/tools/sense/device.py
index a2a6965..c36f32f 100644
--- a/tools/sense/device.py
+++ b/tools/sense/device.py
@@ -37,6 +37,7 @@
from factory_pb import factory_pb2
from pubsub_pb import pubsub_pb2
import morse_code_pb2
+import state_manager_pb2
_LOG = logging.getLogger(__file__)
@@ -151,6 +152,7 @@
factory_pb2,
morse_code_pb2,
pubsub_pb2,
+ state_manager_pb2,
]