mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 01:01:12 -07:00
An ESP32-S3 DevKitC-1 on the bench network drives three relays at the machine's connectors so the acceptance tool can open the lid loop, pull the interlock loop and press the button with nobody in the room. Two contacts are normally closed in loops the safety chain already reads, the third is normally open across the button input and only ever pulsed (20 to 500 ms, the end armed before the line rises); every line is low at boot and after any reset, the task watchdog panics and reboots, and the button channel needs a jumper. HTTP on port 80 under a key in X-Fixture-Key; the hostname announced over DHCP and mDNS. ESP-IDF v5.5 native, the mDNS component pinned in dependencies.lock; fixture.env (git-ignored) is the one input: the wifi, the key, the hostname. fixture.sh builds with idf.py or in the espressif/idf container, flashes with esptool from pip. The decisions that need no hardware live in policy.c with a gcc host test; CI runs it and builds the firmware in the pinned container.
129 lines
3.8 KiB
C
129 lines
3.8 KiB
C
/*
|
|
* forgefixture relays (see relays.h).
|
|
*
|
|
* The safety argument lives here: the lid and interlock contacts are
|
|
* normally closed and the button contact normally open, so a line that
|
|
* is low leaves the machine exactly as it is without the fixture. Every
|
|
* line is driven low first thing at boot, after any reset, and by
|
|
* relays_release(); the button is only ever pulsed, its end set by a
|
|
* one-shot timer that is armed before the line goes high.
|
|
*
|
|
* (C) Copyright 2026
|
|
* Scott Wiederhold, s.e.wiederhold@gmail.com
|
|
* SPDX-License-Identifier: MIT
|
|
*/
|
|
#include "relays.h"
|
|
|
|
#include "driver/gpio.h"
|
|
#include "esp_log.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
|
|
static const char *TAG = "relays";
|
|
|
|
static const gpio_num_t GPIOS[CH_COUNT] = {RELAY_GPIO_LID, RELAY_GPIO_INTERLOCK, RELAY_GPIO_BUTTON};
|
|
|
|
static SemaphoreHandle_t s_lock;
|
|
static esp_timer_handle_t s_pulse_end;
|
|
static bool s_energized[CH_COUNT];
|
|
static bool s_pulsing;
|
|
|
|
static void drive(channel_t ch, bool level)
|
|
{
|
|
gpio_set_level(GPIOS[ch], level ? 1 : 0);
|
|
s_energized[ch] = level;
|
|
}
|
|
|
|
static void pulse_end(void *arg)
|
|
{
|
|
(void)arg;
|
|
xSemaphoreTake(s_lock, portMAX_DELAY);
|
|
drive(CH_BUTTON, false);
|
|
s_pulsing = false;
|
|
xSemaphoreGive(s_lock);
|
|
ESP_LOGI(TAG, "button released");
|
|
}
|
|
|
|
void relays_init(void)
|
|
{
|
|
/* Low before the pins become outputs: the DevKit's pull state at
|
|
* reset is not a relay's idea of off. */
|
|
for (int i = 0; i < CH_COUNT; i++) {
|
|
gpio_reset_pin(GPIOS[i]);
|
|
gpio_set_level(GPIOS[i], 0);
|
|
gpio_set_direction(GPIOS[i], GPIO_MODE_OUTPUT);
|
|
gpio_set_level(GPIOS[i], 0);
|
|
s_energized[i] = false;
|
|
}
|
|
gpio_reset_pin(ENABLE_GPIO_BUTTON);
|
|
gpio_set_direction(ENABLE_GPIO_BUTTON, GPIO_MODE_INPUT);
|
|
gpio_set_pull_mode(ENABLE_GPIO_BUTTON, GPIO_PULLUP_ONLY);
|
|
|
|
s_lock = xSemaphoreCreateMutex();
|
|
const esp_timer_create_args_t args = {
|
|
.callback = pulse_end,
|
|
.name = "button-pulse",
|
|
};
|
|
ESP_ERROR_CHECK(esp_timer_create(&args, &s_pulse_end));
|
|
ESP_LOGI(TAG, "lid GPIO%d, interlock GPIO%d, button GPIO%d (enable jumper GPIO%d): all released",
|
|
RELAY_GPIO_LID, RELAY_GPIO_INTERLOCK, RELAY_GPIO_BUTTON, ENABLE_GPIO_BUTTON);
|
|
}
|
|
|
|
static bool button_enabled(void)
|
|
{
|
|
return gpio_get_level(ENABLE_GPIO_BUTTON) == 0;
|
|
}
|
|
|
|
esp_err_t relays_set_loop(channel_t ch, bool energize)
|
|
{
|
|
if (ch != CH_LID && ch != CH_INTERLOCK)
|
|
return ESP_ERR_INVALID_ARG;
|
|
xSemaphoreTake(s_lock, portMAX_DELAY);
|
|
drive(ch, energize);
|
|
xSemaphoreGive(s_lock);
|
|
ESP_LOGI(TAG, "%s %s", policy_channel_name(ch), policy_state_name(ch, energize));
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t relays_pulse_button(int ms)
|
|
{
|
|
if (!button_enabled())
|
|
return ESP_ERR_NOT_ALLOWED;
|
|
xSemaphoreTake(s_lock, portMAX_DELAY);
|
|
if (s_pulsing) {
|
|
xSemaphoreGive(s_lock);
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
s_pulsing = true;
|
|
/* The end is armed before the line rises. */
|
|
ESP_ERROR_CHECK(esp_timer_start_once(s_pulse_end, (uint64_t)ms * 1000ULL));
|
|
drive(CH_BUTTON, true);
|
|
xSemaphoreGive(s_lock);
|
|
ESP_LOGI(TAG, "button pressed for %d ms", ms);
|
|
return ESP_OK;
|
|
}
|
|
|
|
void relays_release(void)
|
|
{
|
|
xSemaphoreTake(s_lock, portMAX_DELAY);
|
|
esp_timer_stop(s_pulse_end); /* harmless when not running */
|
|
for (int i = 0; i < CH_COUNT; i++)
|
|
drive((channel_t)i, false);
|
|
s_pulsing = false;
|
|
xSemaphoreGive(s_lock);
|
|
ESP_LOGI(TAG, "all released");
|
|
}
|
|
|
|
relays_state_t relays_state(void)
|
|
{
|
|
relays_state_t st;
|
|
xSemaphoreTake(s_lock, portMAX_DELAY);
|
|
for (int i = 0; i < CH_COUNT; i++)
|
|
st.energized[i] = s_energized[i];
|
|
st.button_pulsing = s_pulsing;
|
|
xSemaphoreGive(s_lock);
|
|
st.button_enabled = button_enabled();
|
|
return st;
|
|
}
|