// Project: pa night-light
// Copyright (c) 2023-2024, Framework Labs
#include "proto_activities.h"
#include <Adafruit_NeoPixel.h>
// Types
/// The top-level modes of the system.
enum class Mode {
OFF, STANDBY, ON
};
// Common activities
/// Stops after the given number of ticks.
pa_activity (Delay, pa_ctx(unsigned i), unsigned ticks) {
pa_self.i = ticks;
while (pa_self.i > 0) {
--pa_self.i;
pa_pause;
}
} pa_end;
/// Detects when `value` changes and reports it in the output parameter.
pa_activity (ChangeDetector, pa_ctx(int prevValue), int value, bool& didChange) {
didChange = true;
pa_self.prevValue = value;
pa_pause;
pa_always {
didChange = pa_self.prevValue != value;
pa_self.prevValue = value;
} pa_always_end;
} pa_end;
/// Detects the rising edge of the signal.
pa_activity (RisingEdgeDetector, pa_ctx(), bool signal, bool& edge) {
edge = false;
pa_every (signal) {
edge = true;
pa_pause;
edge = false;
pa_await (!signal);
} pa_every_end;
} pa_end
/// Extends a detected event for the given duration.
pa_activity (EventExtender, pa_ctx(pa_use(Delay)), bool event, unsigned int duration, bool& prolongedEvent) {
prolongedEvent = false;
pa_every (event) {
prolongedEvent = true;
pa_when_reset (event, Delay, duration);
prolongedEvent = false;
} pa_every_end;
} pa_end;
// EMA filter
static int ema(int value, int average, float alpha) {
return static_cast<int>(alpha * value + (1 - alpha) * average);
}
/// A simple Exponential-Moving-Average low pass filter.
pa_activity (EMAFilter, pa_ctx(int average), int inValue, float alpha, int& outValue) {
outValue = pa_self.average = inValue;
pa_pause;
pa_always {
outValue = pa_self.average = ema(inValue, pa_self.average, alpha);
} pa_always_end;
} pa_end;
// Button
static constexpr int BUTTON_PIN = 4;
static void initButton() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
static bool readButton() {
return digitalRead(BUTTON_PIN) == LOW;
}
/// Continuously reads and returns the value of the button pin.
pa_activity (ButtonReader, pa_ctx(), bool& isPressed) {
initButton();
pa_always {
isPressed = readButton();
} pa_always_end;
} pa_end;
/// Returns the rising edge of a button press.
pa_activity (ButtonRecognizer, pa_ctx(pa_co_res(2); pa_use(ButtonReader); pa_use(RisingEdgeDetector); bool rawPressed),
bool& isPressed) {
pa_co(2) {
pa_with (ButtonReader, pa_self.rawPressed);
pa_with (RisingEdgeDetector, pa_self.rawPressed, isPressed);
} pa_co_end;
} pa_end;
// Indicator Led
static constexpr int LED_PIN = 2;
static void initLed() {
pinMode(LED_PIN, OUTPUT);
}
static void setLed() {
digitalWrite(LED_PIN, HIGH);
}
static void clearLed() {
digitalWrite(LED_PIN, LOW);
}
/// Blinks the led with given on-tick and off-tick intervals.
pa_activity (Blinker, pa_ctx(pa_use(Delay)),
unsigned onTicks, unsigned offTicks) {
initLed();
pa_repeat {
setLed();
pa_run (Delay, onTicks);
clearLed();
pa_run (Delay, offTicks);
}
} pa_end;
// Photon Sensor
static constexpr int PHOTON_PIN = 14;
static void initPhotoResistor() {
pinMode(PHOTON_PIN, INPUT);
}
static int readPhotoResistor() {
return analogRead(PHOTON_PIN);
}
/// Reads and filters the photoresistor.
pa_activity (PhotonReader, pa_ctx(), int& level) {
initPhotoResistor();
pa_always {
const auto value = readPhotoResistor();
level = map(value, 0, 1023, 0, 31);
} pa_always_end;
} pa_end;
/// Returns true if a threshold is suprassed.
pa_activity (PhotonThresholder, pa_ctx(), int level, int threshold, bool& triggered) {
pa_always {
triggered = level <= threshold;
} pa_always_end;
} pa_end;
/// Reads and filters the photo-ressistor and returns true if the threshold is surpassed.
pa_activity (PhotonSensor, pa_ctx(pa_co_res(3); pa_use(PhotonReader); pa_use(EMAFilter); pa_use(PhotonThresholder); int rawLevel; int level),
bool& triggered) {
pa_co(3) {
pa_with (PhotonReader, pa_self.rawLevel);
pa_with (EMAFilter, pa_self.rawLevel, 0.4, pa_self.level);
pa_with (PhotonThresholder, pa_self.level, 15, triggered);
} pa_co_end;
} pa_end;
// Brightness Dial
static constexpr int DIAL_PIN = 15;
static void initPotentiometer() {
pinMode(DIAL_PIN, INPUT);
}
static int readPhotentiometer() {
return analogRead(DIAL_PIN);
}
/// Reads the Potentiometer.
pa_activity (BrightnessReader, pa_ctx(), int& brightness) {
initPotentiometer();
pa_always {
const auto value = readPhotentiometer();
brightness = map(value, 0, 4095, 0, 255);
} pa_always_end;
} pa_end;
// Reads and filters the Potentiometer value.
pa_activity (BrightnessDial, pa_ctx(pa_co_res(2); pa_use(BrightnessReader); pa_use(EMAFilter); int rawBrightness), int& brightness) {
pa_co(2) {
pa_with (BrightnessReader, pa_self.rawBrightness);
pa_with (EMAFilter, pa_self.rawBrightness, 0.4, brightness);
} pa_co_end;
} pa_end;
// Light Ring
static constexpr int RING_PIN = 12;
static Adafruit_NeoPixel strip = Adafruit_NeoPixel(16, RING_PIN, NEO_GRB + NEO_KHZ800);
static void initStrip() {
pinMode(RING_PIN, OUTPUT);
strip.begin();
}
/// Calculates the brightness for the light from the ambient light, the day-night condition and if the user is turning the dial.
pa_activity (LightBrightnessCalculator, pa_ctx(), int ambientBrightness, bool isDay, bool isChanging, int& lightBrightness) {
pa_always {
if (isChanging) {
lightBrightness = ambientBrightness;
} else if (isDay) {
lightBrightness = 0;
} else {
lightBrightness = ambientBrightness;
}
} pa_always_end;
} pa_end;
/// Drives the LED strip with the given brightness.
pa_activity (LightDriver, pa_ctx(int prevBrightness), int brightness) {
initStrip();
pa_repeat {
strip.setBrightness(brightness);
for (uint16_t i = 0; i < strip.numPixels(); ++i) {
strip.setPixelColor(i, strip.Color(255, 255, 255));
}
strip.show();
pa_self.prevBrightness = brightness;
pa_await (brightness != pa_self.prevBrightness);
}
} pa_end;
/// Manages the Light by detecting if the user is changing the potentiometer and sets the light according
/// to the ambient brightness and the day-night situation.
pa_activity (Light, pa_ctx(pa_co_res(4); pa_use(ChangeDetector); pa_use(EventExtender); pa_use(LightBrightnessCalculator); pa_use(LightDriver);
bool didChange; bool isChanging; int lightBrightness),
int ambientBrightness, bool isDay) {
pa_co(4) {
pa_with (ChangeDetector, ambientBrightness, pa_self.didChange);
pa_with (EventExtender, pa_self.didChange, 20, pa_self.isChanging);
pa_with (LightBrightnessCalculator, ambientBrightness, isDay, pa_self.isChanging, pa_self.lightBrightness);
pa_with (LightDriver, pa_self.lightBrightness);
} pa_co_end;
} pa_end;
static void clearLight() {
strip.setBrightness(0);
strip.show();
}
// Mode Controller
/// Maps `isDay` bool to `Mode`.
pa_activity (ModeMapper, pa_ctx(), bool isDay, Mode& mode) {
pa_always {
mode = isDay ? Mode::STANDBY : Mode::ON;
} pa_always_end;
} pa_end;
/// Controls the ON mode by reading the sensors, and controling the light brightness with their values.
pa_activity (OnModeController, pa_ctx(pa_co_res(4); pa_use(Light); pa_use(BrightnessDial); pa_use(PhotonSensor); pa_use(ModeMapper);
int brightness; bool isDay),
Mode& mode) {
pa_co(4) {
pa_with (PhotonSensor, pa_self.isDay);
pa_with (BrightnessDial, pa_self.brightness);
pa_with (Light, pa_self.brightness, pa_self.isDay);
pa_with (ModeMapper, pa_self.isDay, mode);
} pa_co_end;
} pa_end;
/// Controlls the OFF mode by shutting off the light.
pa_activity (OffModeController, pa_ctx()) {
clearLight();
pa_halt;
} pa_end;
/// Toggles between ON mode and OFF mode on button press.
pa_activity (ModeController, pa_ctx(pa_use(OnModeController); pa_use(OffModeController)),
bool isPressed, Mode& mode) {
pa_repeat {
pa_when_abort (isPressed, OnModeController, mode);
mode = Mode::OFF;
pa_when_abort (isPressed, OffModeController);
}
} pa_end;
// Mode Indicator
/// Applies the mode to the indication LED.
pa_activity (ModeIndicationApplier, pa_ctx(pa_use(Blinker)), Mode mode) {
if (mode == Mode::OFF) {
clearLed();
pa_halt;
} else if (mode == Mode::STANDBY) {
pa_run (Blinker, 10, 10);
} else { // mode == Mode::ON
setLed();
pa_halt;
}
} pa_end;
/// Drives an LED to indicate the mode.
pa_activity (ModeIndicator, pa_ctx(pa_use(ModeIndicationApplier); Mode prevMode), Mode mode) {
initLed();
pa_repeat {
pa_self.prevMode = mode;
pa_when_abort (mode != pa_self.prevMode, ModeIndicationApplier, mode);
}
} pa_end;
// Main
/// The main activity which sets up the button recognizer, mode-controller and mode-indicator.
pa_activity (Main, pa_ctx(pa_use(ButtonRecognizer); pa_use(ModeController); pa_use(ModeIndicator);
pa_co_res(3); bool isPressed; Mode mode)) {
pa_co(3) {
pa_with (ButtonRecognizer, pa_self.isPressed);
pa_with (ModeController, pa_self.isPressed, pa_self.mode);
pa_with (ModeIndicator, pa_self.mode);
} pa_co_end;
} pa_end;
pa_use(Main);
// Setup and Loop
void setup() {
Serial.begin(9600);
}
void loop() {
pa_tick(Main);
delay(100); // 10Hz
}