Support the legacy towers' DHT11 and fan, and fix the installer's reflash and test steps
This commit is contained in:
parent
8cf3b3b9e9
commit
8d81e03a3f
|
|
@ -95,6 +95,7 @@ add_library(fgc_core STATIC
|
||||||
src/core/ChannelTopics.cpp
|
src/core/ChannelTopics.cpp
|
||||||
src/sensors/I2cBus.cpp
|
src/sensors/I2cBus.cpp
|
||||||
src/sensors/Sht41Protocol.cpp
|
src/sensors/Sht41Protocol.cpp
|
||||||
|
src/sensors/McuEnvSensor.cpp
|
||||||
src/ui/UiSnapshot.cpp
|
src/ui/UiSnapshot.cpp
|
||||||
src/ui/Scroll.cpp
|
src/ui/Scroll.cpp
|
||||||
src/ui/HeadlessUi.cpp
|
src/ui/HeadlessUi.cpp
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,13 @@ axes = yaw, pitch
|
||||||
; is warned about and ignored.
|
; is warned about and ignored.
|
||||||
imu = none
|
imu = none
|
||||||
; Ambient temperature/humidity sensor: a model name, or 'none'. Same rule.
|
; Ambient temperature/humidity sensor: a model name, or 'none'. Same rule.
|
||||||
|
; sht41 on the host's own I2C bus ([Env] below)
|
||||||
|
; dht11 on the motor MCU, which reads it (the legacy towers; needs [Pins] dht)
|
||||||
env = sht41
|
env = sht41
|
||||||
|
; An enclosure fan the motor MCU runs from its DHT11: 'fan', or 'none' (default).
|
||||||
|
; The legacy towers: off to 25 C, +20/255 per degree, full from 37.75 C, and full
|
||||||
|
; whenever the sensor cannot be read. Needs env = dht11 and [Pins] fan_pwm.
|
||||||
|
;cooling = fan
|
||||||
; The [Camera.<name>] blocks expected to be present and found.
|
; The [Camera.<name>] blocks expected to be present and found.
|
||||||
cameras = rgb
|
cameras = rgb
|
||||||
; How much bearing one picture covers, in degrees. Pan mode ('mode pan') cuts
|
; How much bearing one picture covers, in degrees. Pan mode ('mode pan') cuts
|
||||||
|
|
@ -91,6 +97,13 @@ yaw_cs = 8
|
||||||
yaw_en = 7
|
yaw_en = 7
|
||||||
pitch_cs = 1
|
pitch_cs = 1
|
||||||
pitch_en = 0
|
pitch_en = 0
|
||||||
|
; Parts on the MCU itself - the legacy towers (hardware/lattepanda-3-delta/
|
||||||
|
; pins-legacy.ini). Each only with its declaration above; the firmware is built
|
||||||
|
; with them only where they are declared, and is unchanged elsewhere.
|
||||||
|
;dht = 9 ; env = dht11
|
||||||
|
;fan_pwm = 10 ; cooling = fan; a PWM pin (Leonardo: 3 5 6 9 10 11 13)
|
||||||
|
;fan_tach = 11 ; optional; lets the host see a fan that has stopped
|
||||||
|
;spi_idle_cs = 2 ; another device on the drivers' SPI bus, held deselected
|
||||||
|
|
||||||
[IMU]
|
[IMU]
|
||||||
; Only read when [Hardware] imu is not 'none'. On the LattePanda 3 Delta the MTi
|
; Only read when [Hardware] imu is not 'none'. On the LattePanda 3 Delta the MTi
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,42 @@ measured), both edges included - so `mode pan 90 30` takes six pictures, 75°
|
||||||
to 105°. It is a property of the camera and lens, so it lives in
|
to 105°. It is a property of the camera and lens, so it lives in
|
||||||
`hardware.ini`. Must be above 0. See [mqtt-api.md](mqtt-api.md#modes-auto-pan-manual).
|
`hardware.ini`. Must be above 0. See [mqtt-api.md](mqtt-api.md#modes-auto-pan-manual).
|
||||||
|
|
||||||
|
### The legacy towers: a DHT11 and a fan on the motor MCU
|
||||||
|
|
||||||
|
Four towers carry an ambient DHT11 and an enclosure fan wired to the Leonardo
|
||||||
|
itself, not to the host. They are declared, not branched:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Hardware]
|
||||||
|
axes = yaw
|
||||||
|
env = dht11 ; read by the motor MCU
|
||||||
|
cooling = fan ; the MCU runs the fan from it
|
||||||
|
|
||||||
|
[Pins] ; hardware/lattepanda-3-delta/pins-legacy.ini
|
||||||
|
yaw_cs = 8
|
||||||
|
yaw_en = 7
|
||||||
|
dht = 9
|
||||||
|
fan_pwm = 10
|
||||||
|
fan_tach = 11
|
||||||
|
spi_idle_cs = 2
|
||||||
|
```
|
||||||
|
|
||||||
|
- **The firmware is built with them only where they are declared**
|
||||||
|
(`firmware/tools/gen_wiring.py`); a tower without them gets byte-for-byte the
|
||||||
|
firmware it had.
|
||||||
|
- **The fan is the MCU's job**, and keeps running with the host down. The law
|
||||||
|
is the legacy firmware's: off up to 25 °C, +20/255 per degree, full from
|
||||||
|
37.75 °C. The fan runs **at full speed until the first good reading, and
|
||||||
|
after three failed reads in a row**.
|
||||||
|
- **What the host does:** the MCU reports every 10 s as
|
||||||
|
`ENV t=<C> h=<%> ok=<0|1> fan=<duty> rpm=<n>`. The host shows it as the
|
||||||
|
ambient sensor ("DHT11 (ambient, via MCU)"), with the fan's duty and rpm.
|
||||||
|
- **A fan driven at half speed or more that reads 0 rpm on two readings** is
|
||||||
|
logged as a warning, marked `FAN` on the Sensors panel, and fails
|
||||||
|
`test hardware env`. This needs `fan_tach`.
|
||||||
|
- **`spi_idle_cs`** holds a second SPI device's chip select high from power-up,
|
||||||
|
so it never answers on the drivers' bus.
|
||||||
|
|
||||||
### One key, not two: declaring a sensor is what enables it
|
### One key, not two: declaring a sensor is what enables it
|
||||||
|
|
||||||
`[Hardware] imu` and `[Hardware] env` name what is fitted. `[Features] enable_imu`
|
`[Hardware] imu` and `[Hardware] env` name what is fitted. `[Features] enable_imu`
|
||||||
|
|
|
||||||
|
|
@ -833,6 +833,18 @@ Each tower runs its own RabbitMQ, in Docker, with the `rabbitmq_mqtt` and
|
||||||
`rabbitmq_shovel` plugins - the shovel is what forwards this tower's messages to
|
`rabbitmq_shovel` plugins - the shovel is what forwards this tower's messages to
|
||||||
the ground station. That lives in `~/zkms-client/`, outside this repository.
|
the ground station. That lives in `~/zkms-client/`, outside this repository.
|
||||||
|
|
||||||
|
### A legacy tower (DHT11 and fan on the MCU)
|
||||||
|
|
||||||
|
Answer the installer's ambient sensor question with `dht11` and the fan
|
||||||
|
question with yes, and choose `loom = legacy`. The firmware is then built with
|
||||||
|
the DHT11 and fan (see configuration.md, "The legacy towers").
|
||||||
|
|
||||||
|
Check it **on site** after the first flash:
|
||||||
|
1. The fan runs at full speed from power-up.
|
||||||
|
2. It drops once readings come in, if the box is below 25 °C.
|
||||||
|
3. Warming the DHT11 by hand, or with a hair-dryer, spins it up.
|
||||||
|
4. `test hardware env` passes, fan leaf included.
|
||||||
|
|
||||||
### Phase 4 - IMU and ambient sensor
|
### Phase 4 - IMU and ambient sensor
|
||||||
|
|
||||||
Only if fitted, and declared in `hardware.ini` (`imu`, `env`) - the declaration
|
Only if fitted, and declared in `hardware.ini` (`imu`, `env`) - the declaration
|
||||||
|
|
|
||||||
|
|
@ -303,11 +303,36 @@ kv_or_todo() {
|
||||||
printf '%s\n' "$why" | fold -s -w 72 | sed 's/^/; /' | sed 's/[[:space:]]*$//'
|
printf '%s\n' "$why" | fold -s -w 72 | sed 's/^/; /' | sed 's/[[:space:]]*$//'
|
||||||
}
|
}
|
||||||
|
|
||||||
# `--pin k=v` values as a [Pins] block, or the kit's reference loom, or a TODO.
|
# A loom file's lines, keeping only the pins of what this tower declares: a
|
||||||
|
# yaw-only tower drops the pitch lines, one without the DHT11 or fan drops
|
||||||
|
# theirs. The firmware build refuses a pin for an undeclared part, so copying a
|
||||||
|
# loom whole stopped every yaw-only install on the standard Delta loom.
|
||||||
|
kit_loom_file() {
|
||||||
|
case "$1" in
|
||||||
|
standard) echo "$KIT_DIR/pins.ini" ;;
|
||||||
|
legacy) echo "$KIT_DIR/pins-legacy.ini" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
emit_loom() {
|
||||||
|
local file="$1" axes="$2" env="$3" cooling="$4"
|
||||||
|
awk -v axes=",${axes// /}," -v env="$env" -v cooling="$cooling" '
|
||||||
|
/^[[:space:]]*[;#]/ || /^[[:space:]]*\[/ || !index($0, "=") { print; next }
|
||||||
|
{
|
||||||
|
k = substr($0, 1, index($0, "=") - 1); gsub(/[[:space:]]/, "", k)
|
||||||
|
if (k ~ /^pitch_/ && index(axes, ",pitch,") == 0) next
|
||||||
|
if (k == "dht" && env != "dht11") next
|
||||||
|
if (k ~ /^fan_/ && cooling != "fan") next
|
||||||
|
print
|
||||||
|
}' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# `--pin k=v` values as a [Pins] block, or one of the kit's looms, or a TODO.
|
||||||
emit_pins() {
|
emit_pins() {
|
||||||
if [[ $LOOM == standard ]]; then
|
if [[ -n $LOOM ]]; then
|
||||||
[[ -r $KIT_DIR/pins.ini ]] || die "--loom standard: this kit has no pins.ini"
|
local file; file="$(kit_loom_file "$LOOM")"
|
||||||
cat "$KIT_DIR/pins.ini"
|
[[ -r $file ]] || die "--loom $LOOM: this kit has no $(basename "${file:-pins-$LOOM.ini}")"
|
||||||
|
emit_loom "$file" "$AXES" "$ENV_SENSOR" "$COOLING"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
if (( ${#PINS[@]} > 0 )); then
|
if (( ${#PINS[@]} > 0 )); then
|
||||||
|
|
@ -349,6 +374,7 @@ firmware_env = $KIT_FW_ENV
|
||||||
axes = $AXES
|
axes = $AXES
|
||||||
imu = $IMU
|
imu = $IMU
|
||||||
env = $ENV_SENSOR
|
env = $ENV_SENSOR
|
||||||
|
cooling = $COOLING
|
||||||
cameras = $CAMERAS
|
cameras = $CAMERAS
|
||||||
|
|
||||||
[Serial]
|
[Serial]
|
||||||
|
|
@ -363,7 +389,8 @@ EOF
|
||||||
if [[ $IMU != none ]]; then
|
if [[ $IMU != none ]]; then
|
||||||
printf '\n[IMU]\n; %s\ndevice = %s\n' "$KIT_IMU_NOTE" "${IMU_DEVICE:-$KIT_IMU_DEVICE}"
|
printf '\n[IMU]\n; %s\ndevice = %s\n' "$KIT_IMU_NOTE" "${IMU_DEVICE:-$KIT_IMU_DEVICE}"
|
||||||
fi
|
fi
|
||||||
if [[ $ENV_SENSOR != none ]]; then
|
# A DHT11 is read by the motor MCU: there is no bus of the host's to name.
|
||||||
|
if [[ $ENV_SENSOR != none && $ENV_SENSOR != dht11 ]]; then
|
||||||
printf '\n[Env]\n'
|
printf '\n[Env]\n'
|
||||||
printf '%s\n' "$KIT_ENV_NOTE" | sed 's/^/; /'
|
printf '%s\n' "$KIT_ENV_NOTE" | sed 's/^/; /'
|
||||||
printf 'i2c_device = %s\ni2c_addr = 0x44\n' "${I2C_DEVICE:-${KIT_ENV_DEVICE:-SET_THIS_BY_HAND}}"
|
printf 'i2c_device = %s\ni2c_addr = 0x44\n' "${I2C_DEVICE:-${KIT_ENV_DEVICE:-SET_THIS_BY_HAND}}"
|
||||||
|
|
@ -444,13 +471,15 @@ hardware/$KIT_BOARD/install.sh - set up a $KIT_BOARD tower host. Run ON the devi
|
||||||
|
|
||||||
What was ASSEMBLED (a probe cannot tell missing from broken):
|
What was ASSEMBLED (a probe cannot tell missing from broken):
|
||||||
--axes yaw|yaw,pitch (default: $KIT_DEFAULT_AXES)
|
--axes yaw|yaw,pitch (default: $KIT_DEFAULT_AXES)
|
||||||
--imu none|mti --env none|sht41 --cameras rgb|none
|
--imu none|mti --env none|sht41|dht11 --cameras rgb|none
|
||||||
|
--cooling none|fan a fan the motor MCU runs from its DHT11 (legacy towers)
|
||||||
Overrides for what is otherwise probed:
|
Overrides for what is otherwise probed:
|
||||||
--serial PATH the MCU's /dev/serial/by-id path
|
--serial PATH the MCU's /dev/serial/by-id path
|
||||||
--camera-id DEV_... the camera's Vimba id
|
--camera-id DEV_... the camera's Vimba id
|
||||||
--imu-device PATH --i2c-device PATH
|
--imu-device PATH --i2c-device PATH
|
||||||
Wiring ([Pins], which the firmware is built from):
|
Wiring ([Pins], which the firmware is built from):
|
||||||
--loom standard this kit's reference loom (pins.ini)
|
--loom standard|legacy one of this kit's looms (pins.ini, pins-legacy.ini), keeping
|
||||||
|
only the lines for what is declared
|
||||||
--pin KEY=N one pin at a time instead (yaw_cs=1, spi_sck=2, ...)
|
--pin KEY=N one pin at a time instead (yaw_cs=1, spi_sck=2, ...)
|
||||||
The drivetrain, per axis - anything not given is written as a ; TODO:
|
The drivetrain, per axis - anything not given is written as a ; TODO:
|
||||||
--motor AXIS.KEY=VALUE KEY one of: ${KIT_AXIS_KEYS[*]}
|
--motor AXIS.KEY=VALUE KEY one of: ${KIT_AXIS_KEYS[*]}
|
||||||
|
|
@ -463,7 +492,7 @@ EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
kit_main() {
|
kit_main() {
|
||||||
AXES="$KIT_DEFAULT_AXES" IMU="none" ENV_SENSOR="none" CAMERAS="rgb"
|
AXES="$KIT_DEFAULT_AXES" IMU="none" ENV_SENSOR="none" CAMERAS="rgb" COOLING="none"
|
||||||
SERIAL_DEVICE="" CAMERA_ID="" IMU_DEVICE="" I2C_DEVICE="" LOOM=""
|
SERIAL_DEVICE="" CAMERA_ID="" IMU_DEVICE="" I2C_DEVICE="" LOOM=""
|
||||||
DO_DEPS=0 DO_PRE=0 DO_HW=0 DO_VIMBA=0 PROBE_ONLY=0 TO_STDOUT=0 FORCE=0 APT_UPDATE=1
|
DO_DEPS=0 DO_PRE=0 DO_HW=0 DO_VIMBA=0 PROBE_ONLY=0 TO_STDOUT=0 FORCE=0 APT_UPDATE=1
|
||||||
VIMBA_SRC=""
|
VIMBA_SRC=""
|
||||||
|
|
@ -483,6 +512,7 @@ kit_main() {
|
||||||
--axes) AXES="$2"; shift ;;
|
--axes) AXES="$2"; shift ;;
|
||||||
--imu) IMU="$2"; shift ;;
|
--imu) IMU="$2"; shift ;;
|
||||||
--env) ENV_SENSOR="$2"; shift ;;
|
--env) ENV_SENSOR="$2"; shift ;;
|
||||||
|
--cooling) COOLING="$2"; shift ;;
|
||||||
--cameras) CAMERAS="$2"; shift ;;
|
--cameras) CAMERAS="$2"; shift ;;
|
||||||
--serial) SERIAL_DEVICE="$2"; shift ;;
|
--serial) SERIAL_DEVICE="$2"; shift ;;
|
||||||
--camera-id) CAMERA_ID="$2"; shift ;;
|
--camera-id) CAMERA_ID="$2"; shift ;;
|
||||||
|
|
@ -510,7 +540,11 @@ kit_main() {
|
||||||
shift
|
shift
|
||||||
done
|
done
|
||||||
[[ $explicit -eq 0 ]] && { DO_DEPS=1; DO_PRE=1; DO_HW=1; }
|
[[ $explicit -eq 0 ]] && { DO_DEPS=1; DO_PRE=1; DO_HW=1; }
|
||||||
[[ -z $LOOM || $LOOM == standard ]] || die "--loom takes 'standard' (or use --pin)"
|
[[ -z $LOOM || $LOOM == standard || $LOOM == legacy ]] ||
|
||||||
|
die "--loom takes 'standard' or 'legacy' (or use --pin)"
|
||||||
|
[[ $COOLING == none || $COOLING == fan ]] || die "--cooling takes 'none' or 'fan'"
|
||||||
|
[[ $COOLING == none || $ENV_SENSOR == dht11 ]] ||
|
||||||
|
die "--cooling fan needs --env dht11: the fan runs from the DHT11 on the MCU"
|
||||||
[[ -n $LOOM && ${#PINS[@]} -gt 0 ]] && die "--loom and --pin are alternatives; give one"
|
[[ -n $LOOM && ${#PINS[@]} -gt 0 ]] && die "--loom and --pin are alternatives; give one"
|
||||||
|
|
||||||
local first key
|
local first key
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,16 @@ In particular:
|
||||||
not copy them to another tower without reading that tower's boards and
|
not copy them to another tower without reading that tower's boards and
|
||||||
firmware revision the same way.
|
firmware revision the same way.
|
||||||
|
|
||||||
|
## The legacy loom (`pins-legacy.ini`)
|
||||||
|
|
||||||
|
For the four legacy towers: yaw only, with a DHT11 (pin 9) and the enclosure fan
|
||||||
|
it runs (PWM 10, tach 11) on the Leonardo, and a second SPI device's chip select
|
||||||
|
(pin 2) held high. The pins are transcribed from the legacy firmware (a TMC-API
|
||||||
|
Arduino sketch); the yaw pins match the standard loom. Declare it with
|
||||||
|
`env = dht11` and `cooling = fan`. The installer offers it as `loom = legacy`,
|
||||||
|
and keeps only the lines for what is declared. Not yet run on a real legacy
|
||||||
|
tower.
|
||||||
|
|
||||||
## Migrating an existing Delta tower
|
## Migrating an existing Delta tower
|
||||||
|
|
||||||
Host first, firmware second (`docs/hardware-config-plan.md` section 13). The
|
Host first, firmware second (`docs/hardware-config-plan.md` section 13). The
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
; The legacy loom for a LattePanda 3 Delta tower: the ATmega32U4 (Leonardo) ->
|
||||||
|
; one TMC5160 driver board (yaw only), plus parts wired to the MCU itself - an
|
||||||
|
; ambient DHT11 and the enclosure fan it runs, and a second SPI device's chip
|
||||||
|
; select that must be held high.
|
||||||
|
;
|
||||||
|
; Transcribed from the legacy firmware these towers ran (a TMC-API Arduino
|
||||||
|
; sketch): chipCS 8, enable 7, DHTPIN 9, FAN_PWM 10, FAN_SENS 11, chipCS_enc 2.
|
||||||
|
; The yaw pins are the same as the standard loom's.
|
||||||
|
;
|
||||||
|
; Declares with it, in [Hardware]: axes = yaw, env = dht11, cooling = fan.
|
||||||
|
; The installer keeps only the lines for what is declared.
|
||||||
|
[Pins]
|
||||||
|
yaw_cs = 8
|
||||||
|
yaw_en = 7
|
||||||
|
dht = 9
|
||||||
|
fan_pwm = 10
|
||||||
|
fan_tach = 11
|
||||||
|
spi_idle_cs = 2
|
||||||
|
|
@ -39,7 +39,10 @@ struct HardwareConfig {
|
||||||
// This is the state every existing tower is in until it gets a hardware.ini.
|
// This is the state every existing tower is in until it gets a hardware.ini.
|
||||||
std::vector<std::string> axes; // e.g. {"yaw", "pitch"}
|
std::vector<std::string> axes; // e.g. {"yaw", "pitch"}
|
||||||
std::string imu; // sensor model, or "none"
|
std::string imu; // sensor model, or "none"
|
||||||
std::string env; // sensor model, or "none"
|
std::string env; // sensor model, or "none"; dht11 = on the motor MCU
|
||||||
|
// "fan": the motor MCU runs an enclosure fan from its DHT11 (legacy towers).
|
||||||
|
// Needs env = dht11. Empty or "none": no fan.
|
||||||
|
std::string cooling;
|
||||||
std::vector<std::string> cameras; // the [Camera.<name>] blocks expected to exist
|
std::vector<std::string> cameras; // the [Camera.<name>] blocks expected to exist
|
||||||
// How much bearing one picture covers, in degrees. Pan mode steps by it:
|
// How much bearing one picture covers, in degrees. Pan mode steps by it:
|
||||||
// a span is cut into as many pictures as it takes (OperatingMode.h).
|
// a span is cut into as many pictures as it takes (OperatingMode.h).
|
||||||
|
|
@ -59,6 +62,8 @@ struct HardwareConfig {
|
||||||
struct Pins {
|
struct Pins {
|
||||||
long yaw_cs = -1, yaw_en = -1, pitch_cs = -1, pitch_en = -1;
|
long yaw_cs = -1, yaw_en = -1, pitch_cs = -1, pitch_en = -1;
|
||||||
long spi_sck = -1, spi_mosi = -1, spi_miso = -1; // RP2040 only
|
long spi_sck = -1, spi_mosi = -1, spi_miso = -1; // RP2040 only
|
||||||
|
// Parts on the MCU itself (legacy towers). -1 = not wired.
|
||||||
|
long dht = -1, fan_pwm = -1, fan_tach = -1, spi_idle_cs = -1;
|
||||||
bool declared() const { return yaw_cs >= 0 || yaw_en >= 0 || pitch_cs >= 0 ||
|
bool declared() const { return yaw_cs >= 0 || yaw_en >= 0 || pitch_cs >= 0 ||
|
||||||
pitch_en >= 0 || spi_sck >= 0; }
|
pitch_en >= 0 || spi_sck >= 0; }
|
||||||
} pins;
|
} pins;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
|
@ -42,6 +43,17 @@ struct AxisTelemetry {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Telemetry snapshot from the motor controller: one segment per axis.
|
// Telemetry snapshot from the motor controller: one segment per axis.
|
||||||
|
// One "ENV t=<C> h=<%> ok=<0|1> fan=<duty> rpm=<n>" line from the firmware: the
|
||||||
|
// DHT11 on the MCU and the enclosure fan it runs (see firmware src/cooling.h).
|
||||||
|
struct McuEnvReading {
|
||||||
|
int temp_c = 0;
|
||||||
|
int humidity = 0;
|
||||||
|
bool sensor_ok = false; // the last read had a good checksum
|
||||||
|
int fan_duty = 0; // 0-255 as driven; 255 is also the failsafe
|
||||||
|
int fan_rpm = 0; // 0 with no tach wired, or a stopped fan
|
||||||
|
long long epoch_ms = 0; // when the host received it
|
||||||
|
};
|
||||||
|
|
||||||
struct MotorTelemetry {
|
struct MotorTelemetry {
|
||||||
AxisTelemetry yaw;
|
AxisTelemetry yaw;
|
||||||
AxisTelemetry pitch;
|
AxisTelemetry pitch;
|
||||||
|
|
@ -85,6 +97,11 @@ public:
|
||||||
// firmware to ask, and a null implementation would have nothing to say.
|
// firmware to ask, and a null implementation would have nothing to say.
|
||||||
virtual std::vector<std::string> takeNeedLines() { return {}; }
|
virtual std::vector<std::string> takeNeedLines() { return {}; }
|
||||||
|
|
||||||
|
// The latest reading of an ambient DHT11 on the motor MCU and the fan it
|
||||||
|
// drives (firmware "ENV" lines, every 10 s), or nothing on a tower without
|
||||||
|
// one. Thread-safe in implementations. Default: none.
|
||||||
|
virtual std::optional<McuEnvReading> latestEnv() const { return std::nullopt; }
|
||||||
|
|
||||||
// Whether the underlying link is usable.
|
// Whether the underlying link is usable.
|
||||||
virtual bool connected() const = 0;
|
virtual bool connected() const = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ public:
|
||||||
std::string lastDiag() override;
|
std::string lastDiag() override;
|
||||||
unsigned diagSeq() const override;
|
unsigned diagSeq() const override;
|
||||||
std::vector<std::string> takeNeedLines() override;
|
std::vector<std::string> takeNeedLines() override;
|
||||||
|
std::optional<McuEnvReading> latestEnv() const override;
|
||||||
bool connected() const override;
|
bool connected() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,8 @@ std::optional<MotorTelemetry> parseTelemetryLine(const std::string& line);
|
||||||
// Parse a single "Y:..." / "P:..." axis segment. Exposed for unit testing.
|
// Parse a single "Y:..." / "P:..." axis segment. Exposed for unit testing.
|
||||||
std::optional<AxisTelemetry> parseAxisSegment(const std::string& seg);
|
std::optional<AxisTelemetry> parseAxisSegment(const std::string& seg);
|
||||||
|
|
||||||
|
// "ENV t=23 h=45 ok=1 fan=0 rpm=0" -> a reading (epoch_ms left 0). Keys in any
|
||||||
|
// order; t, h and ok required, fan and rpm optional. Nothing for anything else.
|
||||||
|
std::optional<McuEnvReading> parseEnvLine(const std::string& line);
|
||||||
|
|
||||||
} // namespace fgc
|
} // namespace fgc
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fgc/IEnvSensor.h"
|
||||||
|
#include "fgc/IMotorController.h"
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace fgc {
|
||||||
|
|
||||||
|
// A fan driven at half speed or more whose tach reads nothing: dead, jammed or
|
||||||
|
// unplugged. Only meaningful where a tach is wired - without one rpm is always 0.
|
||||||
|
inline bool fanLooksStalled(const McuEnvReading& r) {
|
||||||
|
return r.fan_duty >= 128 && r.fan_rpm == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An ambient DHT11 wired to the motor MCU ([Hardware] env = dht11) - the legacy
|
||||||
|
// towers' sensor. The firmware reads it every 10 s and prints an ENV line; this
|
||||||
|
// serves the latest one through the ordinary IEnvSensor interface, so the
|
||||||
|
// sensors panel, the log and `test hardware env` treat it like any other.
|
||||||
|
//
|
||||||
|
// No reading when the firmware's last read failed its checksum, or when no ENV
|
||||||
|
// line has arrived for 2.5 read periods - a firmware built without the sensor,
|
||||||
|
// or a link that has gone quiet.
|
||||||
|
class McuEnvSensor : public IEnvSensor {
|
||||||
|
public:
|
||||||
|
static constexpr long long kStaleMs = 25000;
|
||||||
|
|
||||||
|
explicit McuEnvSensor(IMotorController& motor, std::function<long long()> now_ms = {});
|
||||||
|
|
||||||
|
void start() override {}
|
||||||
|
void stop() override {}
|
||||||
|
bool connected() const override;
|
||||||
|
std::optional<EnvSample> sample() override;
|
||||||
|
std::string name() const override { return "DHT11"; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::optional<EnvSample> current() const;
|
||||||
|
|
||||||
|
IMotorController& motor_;
|
||||||
|
std::function<long long()> now_ms_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fgc
|
||||||
|
|
@ -95,6 +95,9 @@ struct SensorsView {
|
||||||
// ImuConfigView::problems). Flagged on the compact panel too, because the
|
// ImuConfigView::problems). Flagged on the compact panel too, because the
|
||||||
// readings above are wrong in a way that reading them cannot reveal.
|
// readings above are wrong in a way that reading them cannot reveal.
|
||||||
bool imu_config_alert = false;
|
bool imu_config_alert = false;
|
||||||
|
// An enclosure fan driven hard but not turning (legacy towers). Flagged here
|
||||||
|
// too: it is the fault that stays invisible until the box overheats.
|
||||||
|
bool fan_alert = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// One auto-sweep scan-grid waypoint, for the expanded camera view.
|
// One auto-sweep scan-grid waypoint, for the expanded camera view.
|
||||||
|
|
@ -264,6 +267,14 @@ struct EnvView {
|
||||||
std::string i2c_device; // from EnvConfig, for the bus row
|
std::string i2c_device; // from EnvConfig, for the bus row
|
||||||
unsigned i2c_addr = 0;
|
unsigned i2c_addr = 0;
|
||||||
int period_ms = 0;
|
int period_ms = 0;
|
||||||
|
// A DHT11 on the motor MCU (legacy towers) - no bus of our own to show - and
|
||||||
|
// the enclosure fan the MCU runs from it.
|
||||||
|
bool on_mcu = false;
|
||||||
|
bool fan = false; // [Hardware] cooling = fan
|
||||||
|
bool fan_tach = false; // a tach is wired, so rpm means something
|
||||||
|
int fan_duty = 0; // 0-255
|
||||||
|
int fan_rpm = 0;
|
||||||
|
bool fan_stalled = false; // driven, but the tach reads nothing
|
||||||
};
|
};
|
||||||
|
|
||||||
// Status of the currently-running special operation (calibration, diagnostics,
|
// Status of the currently-running special operation (calibration, diagnostics,
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ firmware_report() {
|
||||||
[[ -n $l ]] && grep -q '^DUMP BEGIN' "$l" && break
|
[[ -n $l ]] && grep -q '^DUMP BEGIN' "$l" && break
|
||||||
sleep 1; l="$(latest_log)"
|
sleep 1; l="$(latest_log)"
|
||||||
done
|
done
|
||||||
[[ -n $l ]] && text="$(grep -E '^DUMP (BEGIN|[YP] state)' "$l" | tail -3)"
|
[[ -n $l ]] && text="$(grep -E '^DUMP (BEGIN|[YP] state|ENV )' "$l" | tail -4)"
|
||||||
else
|
else
|
||||||
local py port; py="$(py_serial)" || return 0
|
local py port; py="$(py_serial)" || return 0
|
||||||
port="$(ini_get "$HW" Serial device 2>/dev/null || true)"
|
port="$(ini_get "$HW" Serial device 2>/dev/null || true)"
|
||||||
|
|
@ -215,7 +215,7 @@ time.sleep(1.5); s.reset_input_buffer(); s.write(b"DUMP\r\n")
|
||||||
end = time.time() + 3
|
end = time.time() + 3
|
||||||
while time.time() < end:
|
while time.time() < end:
|
||||||
l = s.readline().decode(errors="replace").strip()
|
l = s.readline().decode(errors="replace").strip()
|
||||||
if l.startswith("DUMP BEGIN") or (l.startswith("DUMP ") and " state=" in l): print(l)
|
if l.startswith("DUMP BEGIN") or l.startswith("DUMP ENV ") or (l.startswith("DUMP ") and " state=" in l): print(l)
|
||||||
if l.startswith("DUMP END"): break
|
if l.startswith("DUMP END"): break
|
||||||
PYEOF
|
PYEOF
|
||||||
)"
|
)"
|
||||||
|
|
@ -226,17 +226,27 @@ PYEOF
|
||||||
spi="$(sed -n 's/.*DUMP BEGIN.* spi=\([0-9,]*\).*/\1/p' <<< "$text" | tail -1)"
|
spi="$(sed -n 's/.*DUMP BEGIN.* spi=\([0-9,]*\).*/\1/p' <<< "$text" | tail -1)"
|
||||||
y="$(sed -n 's/^DUMP Y .* cs=\([0-9]*\) en=\([0-9]*\).*/\1,\2/p' <<< "$text" | tail -1)"
|
y="$(sed -n 's/^DUMP Y .* cs=\([0-9]*\) en=\([0-9]*\).*/\1,\2/p' <<< "$text" | tail -1)"
|
||||||
p="$(sed -n 's/^DUMP P .* cs=\([0-9]*\) en=\([0-9]*\).*/\1,\2/p' <<< "$text" | tail -1)"
|
p="$(sed -n 's/^DUMP P .* cs=\([0-9]*\) en=\([0-9]*\).*/\1,\2/p' <<< "$text" | tail -1)"
|
||||||
echo "build=${build:-?} spi=${spi:--} Y=${y:--} P=${p:--}"
|
# The parts on the MCU itself (legacy towers): dht,fan_pwm,fan_tach, - when absent.
|
||||||
|
local e="" ln; ln="$(grep '^DUMP ENV ' <<< "$text" | tail -1)"
|
||||||
|
if [[ -n $ln ]]; then
|
||||||
|
local d f t
|
||||||
|
d="$(sed -n 's/.* dht=\([0-9]*\).*/\1/p' <<< "$ln")"
|
||||||
|
f="$(sed -n 's/.* fan_pwm=\([0-9]*\).*/\1/p' <<< "$ln")"
|
||||||
|
t="$(sed -n 's/.* fan_tach=\([0-9]*\).*/\1/p' <<< "$ln")"
|
||||||
|
e="${d:--},${f:--},${t:--}"
|
||||||
|
fi
|
||||||
|
echo "build=${build:-?} spi=${spi:--} Y=${y:--} P=${p:--} E=${e:--}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Does what the firmware reports match what hardware.ini declares? Only the
|
# Does what the firmware reports match what hardware.ini declares? Every field
|
||||||
# declared fields count, as in `test hardware motor`: a Leonardo declares no SPI
|
# counts - an axis or a DHT11 the firmware has and the file does not, or the
|
||||||
# pins (they are fixed), so whatever an older build reports for them is moot.
|
# other way round, needs a rebuild - except undeclared SPI pins: a Leonardo
|
||||||
|
# declares none (they are fixed), so whatever a build reports for them is moot.
|
||||||
pins_match() {
|
pins_match() {
|
||||||
local now="$1" want="$2" f v
|
local now="$1" want="$2" f
|
||||||
[[ -n $now ]] || return 1
|
[[ -n $now ]] || return 1
|
||||||
for f in $want; do
|
for f in $want; do
|
||||||
v="${f#*=}"; [[ $v == - ]] && continue
|
[[ $f == spi=- ]] && continue
|
||||||
[[ " $now " == *" $f "* ]] || return 1
|
[[ " $now " == *" $f "* ]] || return 1
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
@ -252,7 +262,13 @@ declared_pins() {
|
||||||
[[ -n $a ]] && y="$a,$b"
|
[[ -n $a ]] && y="$a,$b"
|
||||||
a="$(ini_get "$HW" Pins pitch_cs 2>/dev/null || true)"; b="$(ini_get "$HW" Pins pitch_en 2>/dev/null || true)"
|
a="$(ini_get "$HW" Pins pitch_cs 2>/dev/null || true)"; b="$(ini_get "$HW" Pins pitch_en 2>/dev/null || true)"
|
||||||
[[ -n $a ]] && p="$a,$b"
|
[[ -n $a ]] && p="$a,$b"
|
||||||
echo "spi=$spi Y=$y P=$p"
|
local e="-"
|
||||||
|
a="$(ini_get "$HW" Pins dht 2>/dev/null || true)"
|
||||||
|
if [[ -n $a ]]; then
|
||||||
|
b="$(ini_get "$HW" Pins fan_pwm 2>/dev/null || true)"; c="$(ini_get "$HW" Pins fan_tach 2>/dev/null || true)"
|
||||||
|
e="$a,${b:--},${c:--}"
|
||||||
|
fi
|
||||||
|
echo "spi=$spi Y=$y P=$p E=$e"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Each gap in hardware.ini, named with its section: "[Motor.pitch] sw_mode".
|
# Each gap in hardware.ini, named with its section: "[Motor.pitch] sw_mode".
|
||||||
|
|
@ -321,13 +337,26 @@ run_test() {
|
||||||
# The app holds the serial port, so a test runs INSIDE it when it is up, and
|
# The app holds the serial port, so a test runs INSIDE it when it is up, and
|
||||||
# as a one-shot run of the binary when it is not.
|
# as a one-shot run of the binary when it is not.
|
||||||
if app_running; then
|
if app_running; then
|
||||||
local pane before after
|
local pane before after restore=""
|
||||||
pane="$(tmux list-panes -t "=${FGC_SESSION:-fgc}" -F '#{pane_id}' | head -1)"
|
pane="$(tmux list-panes -t "=${FGC_SESSION:-fgc}" -F '#{pane_id}' | head -1)"
|
||||||
before="$(ls -t "$LOGDIR"/test_*.log 2>/dev/null | head -1)"
|
before="$(ls -t "$LOGDIR"/test_*.log 2>/dev/null | head -1)"
|
||||||
if tmux capture-pane -p -t "$pane" | grep -q '╭'; then
|
app_cmd() { # one command line into the running program, dashboard or console
|
||||||
tmux send-keys -t "$pane" ':'; sleep 0.5
|
if tmux capture-pane -p -t "$pane" | grep -q '╭'; then
|
||||||
fi
|
tmux send-keys -t "$pane" ':'; sleep 0.5
|
||||||
tmux send-keys -t "$pane" "test $sel" Enter
|
fi
|
||||||
|
tmux send-keys -t "$pane" -l "$1"; tmux send-keys -t "$pane" Enter
|
||||||
|
}
|
||||||
|
# Tests run in manual mode only - auto and pan refuse them. Note the mode,
|
||||||
|
# switch for the test, and put it back afterwards, pan's arguments and all.
|
||||||
|
app_cmd "mode"; sleep 1
|
||||||
|
local m; m="$(grep -oE '\] mode (auto|manual|pan: heading [^,]+, span [^,]+, pitch [^ ]+)' \
|
||||||
|
"$(latest_log)" 2>/dev/null | tail -1 | sed 's/^\] //')"
|
||||||
|
case "$m" in
|
||||||
|
"mode auto") restore="mode auto" ;;
|
||||||
|
"mode pan: "*) restore="$(sed -E 's/mode pan: heading ([^,]+), span ([^,]+), pitch (.+)/mode pan \1 \2 \3/' <<< "$m")" ;;
|
||||||
|
esac
|
||||||
|
if [[ -n $restore ]]; then say "switching to manual for the test (was: $restore)"; app_cmd "mode manual"; sleep 2; fi
|
||||||
|
app_cmd "test $sel"
|
||||||
for _ in $(seq 1 60); do
|
for _ in $(seq 1 60); do
|
||||||
after="$(ls -t "$LOGDIR"/test_*.log 2>/dev/null | head -1)"
|
after="$(ls -t "$LOGDIR"/test_*.log 2>/dev/null | head -1)"
|
||||||
[[ -n $after && $after != "$before" ]] && break
|
[[ -n $after && $after != "$before" ]] && break
|
||||||
|
|
@ -340,10 +369,13 @@ run_test() {
|
||||||
else
|
else
|
||||||
warn "no test report appeared"
|
warn "no test report appeared"
|
||||||
fi
|
fi
|
||||||
TEST_RC=5; return
|
TEST_RC=5
|
||||||
|
[[ -n $restore ]] && app_cmd "$restore"
|
||||||
|
return
|
||||||
fi
|
fi
|
||||||
grep -E '^\[| ; ' "$after" | sed 's/^/ /'
|
grep -E '^\[| ; ' "$after" | sed 's/^/ /'
|
||||||
grep -q '^result = PASS' "$after" && TEST_RC=0 || TEST_RC=4
|
grep -q '^result = PASS' "$after" && TEST_RC=0 || TEST_RC=4
|
||||||
|
if [[ -n $restore ]]; then sleep 2; app_cmd "$restore"; say "back to: $restore"; fi
|
||||||
else
|
else
|
||||||
local out="$WORK/test.out"
|
local out="$WORK/test.out"
|
||||||
TEST_RC=0
|
TEST_RC=0
|
||||||
|
|
@ -452,13 +484,31 @@ declare -a HW_SETS=() # key edits, when there is one
|
||||||
declare -a HW_FILLS=() # "axis key value": a ; TODO answered now, filled in place
|
declare -a HW_FILLS=() # "axis key value": a ; TODO answered now, filled in place
|
||||||
TODO_LEFT=0
|
TODO_LEFT=0
|
||||||
HW_APPEND_PINS=0
|
HW_APPEND_PINS=0
|
||||||
|
# Sets LOOM_A to the loom chosen (standard / legacy), or "" to give each pin.
|
||||||
|
# Returns 0 when a loom was chosen. Only the lines for what the tower declares
|
||||||
|
# are kept, so a yaw-only tower takes the standard loom without its pitch pins.
|
||||||
|
LOOM_A=""
|
||||||
choose_pins() {
|
choose_pins() {
|
||||||
|
local names="standard" n
|
||||||
|
[[ -r $KIT/pins-legacy.ini ]] && names+=" legacy"
|
||||||
echo
|
echo
|
||||||
echo "Wiring ([Pins]) - the firmware is built from it, and a chip-select on the"
|
echo "Wiring ([Pins]) - the firmware is built from it, and a chip-select on the"
|
||||||
echo "wrong pin writes into a driver that was never meant to hear it. The standard"
|
echo "wrong pin writes into a driver that was never meant to hear it. This board's looms:"
|
||||||
echo "loom for this board is hardware/$S_BOARD/pins.ini:"
|
for n in $names; do
|
||||||
sed -n '/^\[Pins\]/,$p' "$KIT/pins.ini" | sed 's/^/ /'
|
echo " $n (hardware/$S_BOARD/$(basename "$(kit_loom_file "$n")")):"
|
||||||
ask_yn loom "Is this tower wired exactly to that loom?" n
|
emit_loom "$(kit_loom_file "$n")" "${AXES_A:-$declared_axes_now}" "${ENV_A:-none}" \
|
||||||
|
"${COOLING_A:-none}" | sed -n '/^\[Pins\]/,$p' | sed 's/^/ /'
|
||||||
|
done
|
||||||
|
while :; do
|
||||||
|
ask loom "Is this tower wired exactly to one of them? (${names// /, }, or n to give each pin)" n
|
||||||
|
case "${REPLY_VAL,,}" in
|
||||||
|
y|yes|standard) LOOM_A=standard; return 0 ;;
|
||||||
|
legacy) [[ $names == *legacy* ]] && { LOOM_A=legacy; return 0; } ;;
|
||||||
|
n|no) LOOM_A=""; return 1 ;;
|
||||||
|
esac
|
||||||
|
warn "'$REPLY_VAL' is not one of: ${names// /, }, n"; unset "ANS[loom]"
|
||||||
|
[[ $NONINTERACTIVE -eq 1 ]] && die "bad value for 'loom'"
|
||||||
|
done
|
||||||
}
|
}
|
||||||
if [[ ! -r $HW ]]; then
|
if [[ ! -r $HW ]]; then
|
||||||
echo
|
echo
|
||||||
|
|
@ -467,9 +517,16 @@ if [[ ! -r $HW ]]; then
|
||||||
ask axes "Axes fitted (yaw, or yaw,pitch)" "$KIT_DEFAULT_AXES"; AXES_A="${REPLY_VAL// /}"
|
ask axes "Axes fitted (yaw, or yaw,pitch)" "$KIT_DEFAULT_AXES"; AXES_A="${REPLY_VAL// /}"
|
||||||
[[ $AXES_A == yaw || $AXES_A == yaw,pitch ]] || die "axes must be 'yaw' or 'yaw,pitch' (the firmware's axis 0 is yaw)"
|
[[ $AXES_A == yaw || $AXES_A == yaw,pitch ]] || die "axes must be 'yaw' or 'yaw,pitch' (the firmware's axis 0 is yaw)"
|
||||||
ask imu "IMU model, or none" none; IMU_A="$REPLY_VAL"
|
ask imu "IMU model, or none" none; IMU_A="$REPLY_VAL"
|
||||||
ask env "Ambient sensor model, or none" none; ENV_A="$REPLY_VAL"
|
ask env "Ambient sensor model (sht41 on the host's I2C, dht11 on the motor MCU), or none" none
|
||||||
|
ENV_A="${REPLY_VAL,,}"
|
||||||
|
COOLING_A=none
|
||||||
|
if [[ $ENV_A == dht11 ]]; then
|
||||||
|
explain "the legacy towers: the motor MCU reads the DHT11 and runs the enclosure fan from it - off to 25 C, full from 37.75 C, full whenever the sensor cannot be read"
|
||||||
|
ask_yn fan "Does the motor MCU run an enclosure fan from it?" y && COOLING_A=fan
|
||||||
|
fi
|
||||||
ask cameras "Cameras (rgb, or none)" "$([[ $HAVE_VIMBA -eq 1 ]] && echo rgb || echo none)"; CAMS_A="$REPLY_VAL"
|
ask cameras "Cameras (rgb, or none)" "$([[ $HAVE_VIMBA -eq 1 ]] && echo rgb || echo none)"; CAMS_A="$REPLY_VAL"
|
||||||
KIT_ARGS=(--stdout --axes "$AXES_A" --imu "$IMU_A" --env "$ENV_A" --cameras "$CAMS_A")
|
KIT_ARGS=(--stdout --axes "$AXES_A" --imu "$IMU_A" --env "$ENV_A" --cooling "$COOLING_A"
|
||||||
|
--cameras "$CAMS_A")
|
||||||
if (( S_MCU_N > 1 )); then
|
if (( S_MCU_N > 1 )); then
|
||||||
echo "Several MCU candidates:"; nl -w2 -s') ' <<< "$S_MCU"
|
echo "Several MCU candidates:"; nl -w2 -s') ' <<< "$S_MCU"
|
||||||
ask serial "Which is the motor MCU (full path)"
|
ask serial "Which is the motor MCU (full path)"
|
||||||
|
|
@ -483,15 +540,17 @@ if [[ ! -r $HW ]]; then
|
||||||
KIT_ARGS+=(--camera-id "$REPLY_VAL")
|
KIT_ARGS+=(--camera-id "$REPLY_VAL")
|
||||||
fi
|
fi
|
||||||
[[ $IMU_A != none ]] && { ask imu_device "IMU serial device" "$KIT_IMU_DEVICE"; KIT_ARGS+=(--imu-device "$REPLY_VAL"); }
|
[[ $IMU_A != none ]] && { ask imu_device "IMU serial device" "$KIT_IMU_DEVICE"; KIT_ARGS+=(--imu-device "$REPLY_VAL"); }
|
||||||
if [[ $ENV_A != none ]]; then
|
if [[ $ENV_A != none && $ENV_A != dht11 ]]; then
|
||||||
ask i2c_device "Ambient sensor I2C bus (check it with i2cdetect -F)" "$KIT_ENV_DEVICE"
|
ask i2c_device "Ambient sensor I2C bus (check it with i2cdetect -F)" "$KIT_ENV_DEVICE"
|
||||||
KIT_ARGS+=(--i2c-device "$REPLY_VAL")
|
KIT_ARGS+=(--i2c-device "$REPLY_VAL")
|
||||||
fi
|
fi
|
||||||
if choose_pins; then KIT_ARGS+=(--loom standard)
|
if choose_pins; then KIT_ARGS+=(--loom "$LOOM_A")
|
||||||
else
|
else
|
||||||
echo "Then give each pin (GPn number):"
|
echo "Then give each pin (GPn number):"
|
||||||
keys=(); [[ $KIT_FW_ENV == *rp2040* ]] && keys+=(spi_sck spi_mosi spi_miso)
|
keys=(); [[ $KIT_FW_ENV == *rp2040* ]] && keys+=(spi_sck spi_mosi spi_miso)
|
||||||
for ax in ${AXES_A//,/ }; do keys+=("${ax}_cs" "${ax}_en"); done
|
for ax in ${AXES_A//,/ }; do keys+=("${ax}_cs" "${ax}_en"); done
|
||||||
|
[[ $ENV_A == dht11 ]] && keys+=(dht)
|
||||||
|
[[ $COOLING_A == fan ]] && keys+=(fan_pwm fan_tach)
|
||||||
for k in "${keys[@]}"; do ask "pin.$k" " $k"; KIT_ARGS+=(--pin "$k=$REPLY_VAL"); done
|
for k in "${keys[@]}"; do ask "pin.$k" " $k"; KIT_ARGS+=(--pin "$k=$REPLY_VAL"); done
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
|
|
@ -521,6 +580,7 @@ else
|
||||||
# An existing file is the tower's record: never regenerated, only topped up.
|
# An existing file is the tower's record: never regenerated, only topped up.
|
||||||
if ! grep -q '^\[Pins\]' "$HW"; then
|
if ! grep -q '^\[Pins\]' "$HW"; then
|
||||||
echo; echo "hardware.ini has no [Pins], and the firmware cannot be built without it."
|
echo; echo "hardware.ini has no [Pins], and the firmware cannot be built without it."
|
||||||
|
declared_axes_now="$(ini_get "$HW" Hardware axes 2>/dev/null | tr -d ' ' || echo yaw)"
|
||||||
choose_pins && HW_APPEND_PINS=1 ||
|
choose_pins && HW_APPEND_PINS=1 ||
|
||||||
warn "add [Pins] to $HW by hand (the format is in config/hardware.example.ini)"
|
warn "add [Pins] to $HW by hand (the format is in config/hardware.example.ini)"
|
||||||
fi
|
fi
|
||||||
|
|
@ -664,7 +724,7 @@ else
|
||||||
grep -q '^\[Pins\]' "$HW" || [[ $HW_APPEND_PINS -eq 1 ]] || todo_n=$((todo_n + 1))
|
grep -q '^\[Pins\]' "$HW" || [[ $HW_APPEND_PINS -eq 1 ]] || todo_n=$((todo_n + 1))
|
||||||
fi
|
fi
|
||||||
(( todo_n > 0 )) && FW_BLOCK="hardware.ini still has $todo_n gap(s): the firmware would sit in NEEDCFG, or not build"
|
(( todo_n > 0 )) && FW_BLOCK="hardware.ini still has $todo_n gap(s): the firmware would sit in NEEDCFG, or not build"
|
||||||
want_pins="$( (HW="$HW_VIEW"; [[ $HW_APPEND_PINS -eq 1 ]] && { cat "$HW" "$KIT/pins.ini" > "$WORK/hw_pins.ini"; HW="$WORK/hw_pins.ini"; }; declared_pins) )"
|
want_pins="$( (HW="$HW_VIEW"; [[ $HW_APPEND_PINS -eq 1 ]] && { cat "$HW" "$(kit_loom_file "$LOOM_A")" > "$WORK/hw_pins.ini"; HW="$WORK/hw_pins.ini"; }; declared_pins) )"
|
||||||
if [[ -n $FW_BLOCK ]]; then plan firmware blocked "$FW_BLOCK"
|
if [[ -n $FW_BLOCK ]]; then plan firmware blocked "$FW_BLOCK"
|
||||||
elif [[ -n $S_FW_NOW && ${S_FW_NOW%% *} == "build=$S_FW_REV" ]] && pins_match "$S_FW_NOW" "$want_pins"; then
|
elif [[ -n $S_FW_NOW && ${S_FW_NOW%% *} == "build=$S_FW_REV" ]] && pins_match "$S_FW_NOW" "$want_pins"; then
|
||||||
plan firmware skip "running $S_FW_REV, built with the declared pins"
|
plan firmware skip "running $S_FW_REV, built with the declared pins"
|
||||||
|
|
@ -765,7 +825,10 @@ elif [[ ${DO[hardware.ini]} == change ]]; then
|
||||||
hr hardware.ini
|
hr hardware.ini
|
||||||
sudo cp -p "$HW" "$HW.bak-$(date +%Y%m%d-%H%M%S)"
|
sudo cp -p "$HW" "$HW.bak-$(date +%Y%m%d-%H%M%S)"
|
||||||
if [[ $HW_APPEND_PINS -eq 1 ]]; then
|
if [[ $HW_APPEND_PINS -eq 1 ]]; then
|
||||||
{ printf '\n'; cat "$KIT/pins.ini"; } | sudo tee -a "$HW" > /dev/null; say "added [Pins]"
|
{ printf '\n'; emit_loom "$(kit_loom_file "$LOOM_A")" "$declared_axes_now" \
|
||||||
|
"$(ini_get "$HW" Hardware env 2>/dev/null || echo none)" \
|
||||||
|
"$(ini_get "$HW" Hardware cooling 2>/dev/null || echo none)"; } |
|
||||||
|
sudo tee -a "$HW" > /dev/null; say "added [Pins]"
|
||||||
fi
|
fi
|
||||||
if (( ${#HW_FILLS[@]} )); then
|
if (( ${#HW_FILLS[@]} )); then
|
||||||
# In place of the ; TODO and its explanation, so the value sits where the
|
# In place of the ; TODO and its explanation, so the value sits where the
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
#include "fgc/mock/MockImuSource.h"
|
#include "fgc/mock/MockImuSource.h"
|
||||||
#include "fgc/mock/MockMotorController.h"
|
#include "fgc/mock/MockMotorController.h"
|
||||||
#include "fgc/mock/NullControlChannel.h"
|
#include "fgc/mock/NullControlChannel.h"
|
||||||
|
#include "fgc/sensors/McuEnvSensor.h"
|
||||||
#include "fgc/sensors/Sht41EnvSensor.h"
|
#include "fgc/sensors/Sht41EnvSensor.h"
|
||||||
#include "fgc/ui/HeadlessUi.h"
|
#include "fgc/ui/HeadlessUi.h"
|
||||||
#include "fgc/ui/IUserInterface.h"
|
#include "fgc/ui/IUserInterface.h"
|
||||||
|
|
@ -282,6 +283,9 @@ struct Application::Impl {
|
||||||
std::atomic<bool> imu_pitch_warned_{false};
|
std::atomic<bool> imu_pitch_warned_{false};
|
||||||
// The TUI heading wizard is aiming by hand; blocks a scan from starting.
|
// The TUI heading wizard is aiming by hand; blocks a scan from starting.
|
||||||
bool heading_setup_active_ = false;
|
bool heading_setup_active_ = false;
|
||||||
|
long long fan_seen_ms_ = 0; // last ENV reading checkFan() judged
|
||||||
|
int fan_stall_count_ = 0; // consecutive stalled readings
|
||||||
|
bool fan_stalled_ = false;
|
||||||
std::vector<HeadingSighting> heading_marks_; // `gimbal heading mark`s this setup
|
std::vector<HeadingSighting> heading_marks_; // `gimbal heading mark`s this setup
|
||||||
|
|
||||||
std::atomic<bool> running{true};
|
std::atomic<bool> running{true};
|
||||||
|
|
@ -363,6 +367,9 @@ struct Application::Impl {
|
||||||
return nullptr; // sensors panel stays "pending"
|
return nullptr; // sensors panel stays "pending"
|
||||||
if (force_mock || cfg.features.mock_env)
|
if (force_mock || cfg.features.mock_env)
|
||||||
return std::make_unique<MockEnvSensor>(std::chrono::milliseconds(cfg.env.period_ms));
|
return std::make_unique<MockEnvSensor>(std::chrono::milliseconds(cfg.env.period_ms));
|
||||||
|
// The legacy towers' DHT11 hangs off the motor MCU, which reads it and
|
||||||
|
// reports each reading on the serial link - there is no bus here to open.
|
||||||
|
if (cfg.hardware.env == "dht11") return std::make_unique<McuEnvSensor>(*motor);
|
||||||
if (cfg.env.i2c_device.empty()) {
|
if (cfg.env.i2c_device.empty()) {
|
||||||
LOG_WARN << "Env sensor enabled but [Env] i2c_device is empty; disabling";
|
LOG_WARN << "Env sensor enabled but [Env] i2c_device is empty; disabling";
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
@ -823,6 +830,25 @@ struct Application::Impl {
|
||||||
s.env.i2c_device = cfg.env.i2c_device;
|
s.env.i2c_device = cfg.env.i2c_device;
|
||||||
s.env.i2c_addr = cfg.env.i2c_addr;
|
s.env.i2c_addr = cfg.env.i2c_addr;
|
||||||
s.env.period_ms = cfg.env.period_ms;
|
s.env.period_ms = cfg.env.period_ms;
|
||||||
|
if (cfg.hardware.env == "dht11") {
|
||||||
|
s.env.on_mcu = true;
|
||||||
|
s.env.period_ms = 10000; // the firmware's own read period
|
||||||
|
}
|
||||||
|
s.env.fan = cfg.hardware.cooling == "fan";
|
||||||
|
s.env.fan_tach = cfg.hardware.pins.fan_tach >= 0;
|
||||||
|
if (auto r = motor ? motor->latestEnv() : std::nullopt) {
|
||||||
|
s.env.fan_duty = r->fan_duty;
|
||||||
|
s.env.fan_rpm = r->fan_rpm;
|
||||||
|
}
|
||||||
|
s.env.fan_stalled = fan_stalled_;
|
||||||
|
s.sensors.env.title = env->name() + (s.env.on_mcu ? " (ambient, via MCU)" : " (ambient)");
|
||||||
|
s.sensors.fan_alert = fan_stalled_;
|
||||||
|
if (s.env.fan) {
|
||||||
|
// The duty as a percentage, and the rpm where a tach says what it is.
|
||||||
|
std::string v = std::to_string(s.env.fan_duty * 100 / 255) + "%";
|
||||||
|
if (s.env.fan_tach) v += " " + std::to_string(s.env.fan_rpm) + " rpm";
|
||||||
|
s.sensors.env.fields.push_back({"Fan", v, "", true});
|
||||||
|
}
|
||||||
if (auto m = env->sample()) {
|
if (auto m = env->sample()) {
|
||||||
s.env.present = true;
|
s.env.present = true;
|
||||||
s.env.temp_c = m->temp_c;
|
s.env.temp_c = m->temp_c;
|
||||||
|
|
@ -1057,6 +1083,25 @@ struct Application::Impl {
|
||||||
// Warn (once per excursion) when an axis's live encoder error |xactual-xenc|
|
// Warn (once per excursion) when an axis's live encoder error |xactual-xenc|
|
||||||
// crosses the configured limit, so positional slip is visible in the log
|
// crosses the configured limit, so positional slip is visible in the log
|
||||||
// without watching the gimbal view. Hysteresis re-arms at 75% of the limit.
|
// without watching the gimbal view. Hysteresis re-arms at 75% of the limit.
|
||||||
|
// A legacy tower's enclosure fan, driven hard but not turning, on two
|
||||||
|
// readings in a row (20 s): said once, and again when it recovers. Nothing
|
||||||
|
// on a tower without a fan, or without a tach to tell.
|
||||||
|
void checkFan() {
|
||||||
|
if (cfg.hardware.cooling != "fan" || cfg.hardware.pins.fan_tach < 0) return;
|
||||||
|
const auto r = motor->latestEnv();
|
||||||
|
if (!r || r->epoch_ms == fan_seen_ms_) return;
|
||||||
|
fan_seen_ms_ = r->epoch_ms;
|
||||||
|
fan_stall_count_ = fanLooksStalled(*r) ? fan_stall_count_ + 1 : 0;
|
||||||
|
const bool stalled = fan_stall_count_ >= 2;
|
||||||
|
if (stalled && !fan_stalled_)
|
||||||
|
LOG_WARN << "enclosure fan driven at " << r->fan_duty
|
||||||
|
<< "/255 but its tach reads 0 rpm - dead, jammed or unplugged; the box "
|
||||||
|
"is not being cooled (" << r->temp_c << " C inside)";
|
||||||
|
else if (!stalled && fan_stalled_)
|
||||||
|
LOG_INFO << "enclosure fan turning again (" << r->fan_rpm << " rpm)";
|
||||||
|
fan_stalled_ = stalled;
|
||||||
|
}
|
||||||
|
|
||||||
void checkTrackingError() {
|
void checkTrackingError() {
|
||||||
const long limit = cfg.enc_error_warn_counts;
|
const long limit = cfg.enc_error_warn_counts;
|
||||||
if (limit <= 0) return; // disabled
|
if (limit <= 0) return; // disabled
|
||||||
|
|
@ -2396,6 +2441,11 @@ struct Application::Impl {
|
||||||
resume_pan_after_test_ = scheduler->pan();
|
resume_pan_after_test_ = scheduler->pan();
|
||||||
if (resume_mode_after_test_ != Mode::Manual)
|
if (resume_mode_after_test_ != Mode::Manual)
|
||||||
setMode(Mode::Manual); // free the serial link; don't fight the routine's moves
|
setMode(Mode::Manual); // free the serial link; don't fight the routine's moves
|
||||||
|
// Manual mode stops the camera's stream, and `test camera` counts the
|
||||||
|
// frames it delivers: without this, every test run in manual - which is
|
||||||
|
// every typed one - failed "the camera delivers whole frames" on a camera
|
||||||
|
// that was fine. Left running afterwards, as a snapshot leaves it.
|
||||||
|
if (camera) camera->start();
|
||||||
LOG_INFO << "starting hardware test (profile '" << profile.name << "')";
|
LOG_INFO << "starting hardware test (profile '" << profile.name << "')";
|
||||||
// What `test hardware` needs beyond the motor and IMU: the declaration to
|
// What `test hardware` needs beyond the motor and IMU: the declaration to
|
||||||
// check against, and the subsystems that answer for themselves.
|
// check against, and the subsystems that answer for themselves.
|
||||||
|
|
@ -2990,6 +3040,7 @@ struct Application::Impl {
|
||||||
pollBackgroundResults(); // apply calibration result / emit DIAG summary+log
|
pollBackgroundResults(); // apply calibration result / emit DIAG summary+log
|
||||||
scheduler->tick();
|
scheduler->tick();
|
||||||
checkTrackingError(); // warn on live encoder-error excursions
|
checkTrackingError(); // warn on live encoder-error excursions
|
||||||
|
checkFan(); // a legacy tower's fan, driven but not turning
|
||||||
publishSnapshot();
|
publishSnapshot();
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -246,6 +246,16 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
|
||||||
cfg.hardware.cameras = splitList(get(kv, "Hardware.cameras"));
|
cfg.hardware.cameras = splitList(get(kv, "Hardware.cameras"));
|
||||||
cfg.hardware.imu = lowered(get(kv, "Hardware.imu"));
|
cfg.hardware.imu = lowered(get(kv, "Hardware.imu"));
|
||||||
cfg.hardware.env = lowered(get(kv, "Hardware.env"));
|
cfg.hardware.env = lowered(get(kv, "Hardware.env"));
|
||||||
|
cfg.hardware.cooling = lowered(get(kv, "Hardware.cooling"));
|
||||||
|
// The same rules the firmware build applies (firmware/tools/gen_wiring.py):
|
||||||
|
// the fan is run from the DHT11's temperature, on the MCU.
|
||||||
|
if (!cfg.hardware.cooling.empty() && cfg.hardware.cooling != "none" &&
|
||||||
|
cfg.hardware.cooling != "fan")
|
||||||
|
throw std::runtime_error("Hardware.cooling must be 'none' or 'fan' (got '" +
|
||||||
|
cfg.hardware.cooling + "')");
|
||||||
|
if (cfg.hardware.cooling == "fan" && cfg.hardware.env != "dht11")
|
||||||
|
throw std::runtime_error("Hardware.cooling = fan needs Hardware.env = dht11: the fan "
|
||||||
|
"is driven from the temperature the motor MCU reads");
|
||||||
{
|
{
|
||||||
auto& p = cfg.hardware.pins;
|
auto& p = cfg.hardware.pins;
|
||||||
p.yaw_cs = getInt(kv, "Pins.yaw_cs", -1);
|
p.yaw_cs = getInt(kv, "Pins.yaw_cs", -1);
|
||||||
|
|
@ -255,6 +265,10 @@ AppConfig ConfigLoader::fromMap(const std::map<std::string, std::string>& kv) {
|
||||||
p.spi_sck = getInt(kv, "Pins.spi_sck", -1);
|
p.spi_sck = getInt(kv, "Pins.spi_sck", -1);
|
||||||
p.spi_mosi = getInt(kv, "Pins.spi_mosi", -1);
|
p.spi_mosi = getInt(kv, "Pins.spi_mosi", -1);
|
||||||
p.spi_miso = getInt(kv, "Pins.spi_miso", -1);
|
p.spi_miso = getInt(kv, "Pins.spi_miso", -1);
|
||||||
|
p.dht = getInt(kv, "Pins.dht", -1);
|
||||||
|
p.fan_pwm = getInt(kv, "Pins.fan_pwm", -1);
|
||||||
|
p.fan_tach = getInt(kv, "Pins.fan_tach", -1);
|
||||||
|
p.spi_idle_cs = getInt(kv, "Pins.spi_idle_cs", -1);
|
||||||
}
|
}
|
||||||
if (kv.count("Hardware.pan_step_deg")) {
|
if (kv.count("Hardware.pan_step_deg")) {
|
||||||
cfg.hardware.pan_step_deg = getDouble(kv, "Hardware.pan_step_deg", 0.0);
|
cfg.hardware.pan_step_deg = getDouble(kv, "Hardware.pan_step_deg", 0.0);
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,12 @@ PresenceReport reconcilePresence(const HardwareConfig& declared, const FoundHard
|
||||||
// ---- env sensor ----
|
// ---- env sensor ----
|
||||||
if (names(declared.env) && !found.env)
|
if (names(declared.env) && !found.env)
|
||||||
missing(r, "env",
|
missing(r, "env",
|
||||||
"declared as '" + declared.env +
|
"declared as '" + declared.env + "' but no ambient sensor came up -- " +
|
||||||
"' but no ambient sensor came up -- check [Env] i2c_device, and that it is a "
|
(declared.env == "dht11"
|
||||||
"real I2C adapter rather than SMBus-only");
|
? std::string("the motor MCU reads it: was the firmware built with "
|
||||||
|
"[Pins] dht?")
|
||||||
|
: std::string("check [Env] i2c_device, and that it is a real I2C "
|
||||||
|
"adapter rather than SMBus-only")));
|
||||||
if (stated(declared.env) && !names(declared.env) && found.env)
|
if (stated(declared.env) && !names(declared.env) && found.env)
|
||||||
undeclared(r, "env", "running, but [Hardware] env says none -- the declaration is stale");
|
undeclared(r, "env", "running, but [Hardware] env says none -- the declaration is stale");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,4 +95,31 @@ std::optional<MotorTelemetry> parseTelemetryLine(const std::string& line) {
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::optional<McuEnvReading> parseEnvLine(const std::string& line) {
|
||||||
|
if (line.rfind("ENV ", 0) != 0) return std::nullopt;
|
||||||
|
McuEnvReading r;
|
||||||
|
bool t = false, h = false, ok = false;
|
||||||
|
std::istringstream in(line.substr(4));
|
||||||
|
for (std::string kv; in >> kv;) {
|
||||||
|
const auto eq = kv.find('=');
|
||||||
|
if (eq == std::string::npos) continue;
|
||||||
|
const std::string k = kv.substr(0, eq);
|
||||||
|
int v = 0;
|
||||||
|
try {
|
||||||
|
size_t used = 0;
|
||||||
|
v = std::stoi(kv.substr(eq + 1), &used);
|
||||||
|
if (used != kv.size() - eq - 1) return std::nullopt;
|
||||||
|
} catch (const std::exception&) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
if (k == "t") { r.temp_c = v; t = true; }
|
||||||
|
else if (k == "h") { r.humidity = v; h = true; }
|
||||||
|
else if (k == "ok") { r.sensor_ok = v != 0; ok = true; }
|
||||||
|
else if (k == "fan") r.fan_duty = v;
|
||||||
|
else if (k == "rpm") r.fan_rpm = v;
|
||||||
|
}
|
||||||
|
if (!t || !h || !ok) return std::nullopt;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace fgc
|
} // namespace fgc
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
#include "fgc/TestRunner.h"
|
#include "fgc/TestRunner.h"
|
||||||
|
|
||||||
|
#include "fgc/sensors/McuEnvSensor.h"
|
||||||
|
|
||||||
#include "fgc/Calibration.h" // unwrapNear
|
#include "fgc/Calibration.h" // unwrapNear
|
||||||
|
|
||||||
#include "fgc/DiagParser.h"
|
#include "fgc/DiagParser.h"
|
||||||
|
|
@ -1047,7 +1049,7 @@ void TestRunner::testHwImu(TestReport& r) {
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestRunner::testHwEnv(TestReport& r) {
|
void TestRunner::testHwEnv(TestReport& r) {
|
||||||
TestSection s; s.id = "hw_env"; s.title = "declared ambient sensor, on a real I2C bus";
|
TestSection s; s.id = "hw_env"; s.title = "declared ambient sensor, and the fan it runs";
|
||||||
setProgress(s.id, 1, 2, "probing bus");
|
setProgress(s.id, 1, 2, "probing bus");
|
||||||
|
|
||||||
const bool declared =
|
const bool declared =
|
||||||
|
|
@ -1060,9 +1062,14 @@ void TestRunner::testHwEnv(TestReport& r) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A DHT11 on the motor MCU (legacy towers) has no bus of ours to check: the
|
||||||
|
// firmware reads it, and its ENV lines are the reading below.
|
||||||
|
const bool on_mcu = hw_.declared->env == "dht11";
|
||||||
|
if (on_mcu) {
|
||||||
|
s.notes.push_back("dht11: read by the motor MCU, not on an I2C bus of this host");
|
||||||
// The adapter class first: an SMBus-only bus makes every later symptom look
|
// The adapter class first: an SMBus-only bus makes every later symptom look
|
||||||
// like a dead sensor, so naming it first is what saves the debugging time.
|
// like a dead sensor, so naming it first is what saves the debugging time.
|
||||||
if (hw_.i2c_device.empty()) {
|
} else if (hw_.i2c_device.empty()) {
|
||||||
CheckResult c;
|
CheckResult c;
|
||||||
c.pass = false;
|
c.pass = false;
|
||||||
c.detail = "declared as '" + hw_.declared->env + "' but [Env] i2c_device is empty";
|
c.detail = "declared as '" + hw_.declared->env + "' but [Env] i2c_device is empty";
|
||||||
|
|
@ -1094,6 +1101,13 @@ void TestRunner::testHwEnv(TestReport& r) {
|
||||||
"what enables it";
|
"what enables it";
|
||||||
} else if (auto sample = hw_.env->sample()) {
|
} else if (auto sample = hw_.env->sample()) {
|
||||||
m.detail = "CRC-valid reading from " + hw_.env->name();
|
m.detail = "CRC-valid reading from " + hw_.env->name();
|
||||||
|
} else if (on_mcu) {
|
||||||
|
m.pass = false;
|
||||||
|
m.detail = motor_.latestEnv()
|
||||||
|
? "the firmware reports the DHT11 but its reads are failing the checksum, "
|
||||||
|
"or its ENV lines stopped -- check the sensor and its wiring"
|
||||||
|
: "no ENV line from the firmware -- was it built with [Pins] dht? "
|
||||||
|
"(re-flash after declaring the sensor)";
|
||||||
} else {
|
} else {
|
||||||
m.pass = false;
|
m.pass = false;
|
||||||
m.detail = "no valid reading within the staleness window -- every transfer is failing, or "
|
m.detail = "no valid reading within the staleness window -- every transfer is failing, or "
|
||||||
|
|
@ -1101,6 +1115,29 @@ void TestRunner::testHwEnv(TestReport& r) {
|
||||||
}
|
}
|
||||||
verdict(s, "env_reading", m);
|
verdict(s, "env_reading", m);
|
||||||
|
|
||||||
|
// The enclosure fan the MCU runs from that reading. Driven hard but reading
|
||||||
|
// no rpm is a dead or unplugged fan - the one fault that would otherwise stay
|
||||||
|
// invisible until the box overheats.
|
||||||
|
if (hw_.declared->cooling == "fan") {
|
||||||
|
CheckResult f;
|
||||||
|
const auto r = motor_.latestEnv();
|
||||||
|
if (!r) {
|
||||||
|
f.pass = false;
|
||||||
|
f.detail = "no ENV line from the firmware, so nothing is known about the fan";
|
||||||
|
} else if (hw_.declared->pins.fan_tach < 0) {
|
||||||
|
f.detail = "driven at " + std::to_string(r->fan_duty) +
|
||||||
|
"/255; no tach wired, so whether it turns is not known";
|
||||||
|
} else if (fanLooksStalled(*r)) {
|
||||||
|
f.pass = false;
|
||||||
|
f.detail = "driven at " + std::to_string(r->fan_duty) +
|
||||||
|
"/255 but the tach reads 0 rpm -- dead, jammed or unplugged";
|
||||||
|
} else {
|
||||||
|
f.detail = "driven at " + std::to_string(r->fan_duty) + "/255, " +
|
||||||
|
std::to_string(r->fan_rpm) + " rpm";
|
||||||
|
}
|
||||||
|
verdict(s, "fan", f);
|
||||||
|
}
|
||||||
|
|
||||||
rollup(s);
|
rollup(s);
|
||||||
r.sections.push_back(std::move(s));
|
r.sections.push_back(std::move(s));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
#include "fgc/sensors/McuEnvSensor.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
namespace fgc {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
long long epochMs() {
|
||||||
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
McuEnvSensor::McuEnvSensor(IMotorController& motor, std::function<long long()> now_ms)
|
||||||
|
: motor_(motor), now_ms_(now_ms ? std::move(now_ms) : epochMs) {}
|
||||||
|
|
||||||
|
std::optional<EnvSample> McuEnvSensor::current() const {
|
||||||
|
const auto r = motor_.latestEnv();
|
||||||
|
if (!r || !r->sensor_ok) return std::nullopt;
|
||||||
|
if (now_ms_() - r->epoch_ms > kStaleMs) return std::nullopt;
|
||||||
|
EnvSample s;
|
||||||
|
s.valid = true;
|
||||||
|
s.temp_c = static_cast<float>(r->temp_c);
|
||||||
|
s.humidity_pct = static_cast<float>(r->humidity);
|
||||||
|
s.timestamp_ms = r->epoch_ms;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool McuEnvSensor::connected() const { return current().has_value(); }
|
||||||
|
|
||||||
|
std::optional<EnvSample> McuEnvSensor::sample() { return current(); }
|
||||||
|
|
||||||
|
} // namespace fgc
|
||||||
|
|
@ -83,6 +83,9 @@ struct SerialMotorController::Impl {
|
||||||
// NEED capture: single lines, queued for the control thread to answer once.
|
// NEED capture: single lines, queued for the control thread to answer once.
|
||||||
std::vector<std::string> need_lines;
|
std::vector<std::string> need_lines;
|
||||||
|
|
||||||
|
// ENV: the latest reading of a DHT11 on the MCU and its fan (under mutex).
|
||||||
|
std::optional<McuEnvReading> latest_env;
|
||||||
|
|
||||||
// Lines waiting to go out, oldest first. Touched only on the io_context
|
// Lines waiting to go out, oldest first. Touched only on the io_context
|
||||||
// thread (every access is inside a posted handler or a completion), so it
|
// thread (every access is inside a posted handler or a completion), so it
|
||||||
// needs no lock.
|
// needs no lock.
|
||||||
|
|
@ -171,9 +174,21 @@ struct SerialMotorController::Impl {
|
||||||
captureDump(line);
|
captureDump(line);
|
||||||
captureDiag(line);
|
captureDiag(line);
|
||||||
captureNeed(line);
|
captureNeed(line);
|
||||||
|
captureEnv(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the latest ENV line: one every 10 s from a tower with a DHT11 on the
|
||||||
|
// MCU, never from one without.
|
||||||
|
void captureEnv(const std::string& line) {
|
||||||
|
auto r = parseEnvLine(line);
|
||||||
|
if (!r) return;
|
||||||
|
r->epoch_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||||
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
|
latest_env = *r;
|
||||||
|
}
|
||||||
|
|
||||||
// Queue a NEED line for the control thread to answer. Unlike DUMP and DIAG
|
// Queue a NEED line for the control thread to answer. Unlike DUMP and DIAG
|
||||||
// this is not a block to assemble - each line stands alone and is actionable
|
// this is not a block to assemble - each line stands alone and is actionable
|
||||||
// immediately, so it is queued rather than accumulated, and taking it clears
|
// immediately, so it is queued rather than accumulated, and taking it clears
|
||||||
|
|
@ -328,6 +343,11 @@ std::string SerialMotorController::lastDiag() {
|
||||||
|
|
||||||
unsigned SerialMotorController::diagSeq() const { return impl_->diag_seq.load(); }
|
unsigned SerialMotorController::diagSeq() const { return impl_->diag_seq.load(); }
|
||||||
|
|
||||||
|
std::optional<McuEnvReading> SerialMotorController::latestEnv() const {
|
||||||
|
std::lock_guard<std::mutex> lock(impl_->mutex);
|
||||||
|
return impl_->latest_env;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<std::string> SerialMotorController::takeNeedLines() {
|
std::vector<std::string> SerialMotorController::takeNeedLines() {
|
||||||
std::lock_guard<std::mutex> lock(impl_->mutex);
|
std::lock_guard<std::mutex> lock(impl_->mutex);
|
||||||
std::vector<std::string> out;
|
std::vector<std::string> out;
|
||||||
|
|
|
||||||
|
|
@ -368,9 +368,10 @@ Element sensorsPanel(const SensorsView& s) {
|
||||||
Element imu_badge = s.imu_config_alert
|
Element imu_badge = s.imu_config_alert
|
||||||
? (text(" IMU CFG ") | color(Color::Red) | bold)
|
? (text(" IMU CFG ") | color(Color::Red) | bold)
|
||||||
: text("");
|
: text("");
|
||||||
|
Element fan_badge = s.fan_alert ? (text(" FAN ") | color(Color::Red) | bold) : text("");
|
||||||
return panel("[s] SENSORS", Color::Magenta,
|
return panel("[s] SENSORS", Color::Magenta,
|
||||||
vbox({group(s.imu, std::move(imu_badge)), separator(),
|
vbox({group(s.imu, std::move(imu_badge)), separator(),
|
||||||
group(s.env, text(""))}));
|
group(s.env, std::move(fan_badge))}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Element cameraPanel(const CaptureView& c) {
|
Element cameraPanel(const CaptureView& c) {
|
||||||
|
|
@ -1057,7 +1058,9 @@ Element sensorsDetailPanel(const ImuView& v, const EnvView& e, ScrollState& st)
|
||||||
};
|
};
|
||||||
body.push_back(separator());
|
body.push_back(separator());
|
||||||
body.push_back(text((e.name.empty() ? std::string("SHT41") : e.name) +
|
body.push_back(text((e.name.empty() ? std::string("SHT41") : e.name) +
|
||||||
" \xE2\x80\x94 ambient (I2C)") | bold | color(Color::Magenta));
|
(e.on_mcu ? " \xE2\x80\x94 ambient (on the motor MCU)"
|
||||||
|
: " \xE2\x80\x94 ambient (I2C)")) |
|
||||||
|
bold | color(Color::Magenta));
|
||||||
if (!e.enabled) {
|
if (!e.enabled) {
|
||||||
body.push_back(text("(env sensor disabled)") | dim);
|
body.push_back(text("(env sensor disabled)") | dim);
|
||||||
body.push_back(
|
body.push_back(
|
||||||
|
|
@ -1078,7 +1081,19 @@ Element sensorsDetailPanel(const ImuView& v, const EnvView& e, ScrollState& st)
|
||||||
}
|
}
|
||||||
// The configured bus is meaningless for the mock, so only show it for real
|
// The configured bus is meaningless for the mock, so only show it for real
|
||||||
// hardware — where it is exactly what you need to debug a missing reading.
|
// hardware — where it is exactly what you need to debug a missing reading.
|
||||||
if (e.enabled && !e.mock) {
|
if (e.fan) {
|
||||||
|
std::string v = std::to_string(e.fan_duty) + "/255";
|
||||||
|
v += e.fan_tach ? ", " + std::to_string(e.fan_rpm) + " rpm" : ", no tach wired";
|
||||||
|
body.push_back(envRow("FAN", e.fan_stalled
|
||||||
|
? text(v + " NOT TURNING") | color(Color::Red) | bold
|
||||||
|
: text(v) | bold));
|
||||||
|
body.push_back(text("off to 25 \xC2\xB0""C, full from 37.75 \xC2\xB0""C, full "
|
||||||
|
"while the sensor is unread") | dim);
|
||||||
|
}
|
||||||
|
if (e.on_mcu) {
|
||||||
|
body.push_back(envRow("Bus", text("read by the motor MCU, every " +
|
||||||
|
std::to_string(e.period_ms) + " ms") | dim));
|
||||||
|
} else if (e.enabled && !e.mock) {
|
||||||
char addr[16];
|
char addr[16];
|
||||||
std::snprintf(addr, sizeof(addr), "0x%02X", e.i2c_addr);
|
std::snprintf(addr, sizeof(addr), "0x%02X", e.i2c_addr);
|
||||||
body.push_back(envRow("Bus", text(e.i2c_device + " @ " + addr + ", every " +
|
body.push_back(envRow("Bus", text(e.i2c_device + " @ " + addr + ", every " +
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ add_executable(fgc_tests
|
||||||
test_iniedit.cpp
|
test_iniedit.cpp
|
||||||
test_camerastall.cpp
|
test_camerastall.cpp
|
||||||
test_remotecommands.cpp
|
test_remotecommands.cpp
|
||||||
|
test_mcuenv.cpp
|
||||||
test_operatingmode.cpp
|
test_operatingmode.cpp
|
||||||
test_tmccurrent.cpp
|
test_tmccurrent.cpp
|
||||||
test_hardwarepresence.cpp
|
test_hardwarepresence.cpp
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
#include <doctest/doctest.h>
|
||||||
|
|
||||||
|
#include "fgc/Config.h"
|
||||||
|
#include "fgc/TelemetryParser.h"
|
||||||
|
#include "fgc/sensors/McuEnvSensor.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
using namespace fgc;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Only latestEnv() matters here; the rest is the minimum the interface demands.
|
||||||
|
struct EnvOnlyMotor : IMotorController {
|
||||||
|
std::optional<McuEnvReading> env;
|
||||||
|
void start() override {}
|
||||||
|
void stop() override {}
|
||||||
|
void sendCommand(const std::string&) override {}
|
||||||
|
MotorTelemetry telemetry() override { return {}; }
|
||||||
|
std::string lastDump() override { return ""; }
|
||||||
|
std::string lastDiag() override { return ""; }
|
||||||
|
unsigned diagSeq() const override { return 0; }
|
||||||
|
bool connected() const override { return true; }
|
||||||
|
std::optional<McuEnvReading> latestEnv() const override { return env; }
|
||||||
|
};
|
||||||
|
|
||||||
|
McuEnvReading reading(int t, int h, bool ok, int fan, int rpm, long long at) {
|
||||||
|
McuEnvReading r;
|
||||||
|
r.temp_c = t; r.humidity = h; r.sensor_ok = ok; r.fan_duty = fan; r.fan_rpm = rpm;
|
||||||
|
r.epoch_ms = at;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("an ENV line from the firmware parses, keys in any order") {
|
||||||
|
auto r = parseEnvLine("ENV t=23 h=45 ok=1 fan=0 rpm=0");
|
||||||
|
REQUIRE(r);
|
||||||
|
CHECK(r->temp_c == 23);
|
||||||
|
CHECK(r->humidity == 45);
|
||||||
|
CHECK(r->sensor_ok);
|
||||||
|
CHECK(r->fan_duty == 0);
|
||||||
|
r = parseEnvLine("ENV rpm=1180 fan=255 ok=0 h=51 t=-4");
|
||||||
|
REQUIRE(r);
|
||||||
|
CHECK(r->temp_c == -4);
|
||||||
|
CHECK_FALSE(r->sensor_ok);
|
||||||
|
CHECK(r->fan_duty == 255);
|
||||||
|
CHECK(r->fan_rpm == 1180);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("anything that is not a whole ENV line is not a reading") {
|
||||||
|
CHECK_FALSE(parseEnvLine("ST Y:A,1,2,0,0,0,0,S"));
|
||||||
|
CHECK_FALSE(parseEnvLine("DUMP ENV t=23 h=45 ok=1")); // the dump's copy, handled there
|
||||||
|
CHECK_FALSE(parseEnvLine("ENV t=23 h=45")); // no ok: cannot tell good from bad
|
||||||
|
CHECK_FALSE(parseEnvLine("ENV t=2x h=45 ok=1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("the MCU sensor serves a fresh good reading, and nothing else") {
|
||||||
|
EnvOnlyMotor motor;
|
||||||
|
long long now = 1'000'000;
|
||||||
|
McuEnvSensor s(motor, [&] { return now; });
|
||||||
|
|
||||||
|
CHECK_FALSE(s.sample()); // no ENV line yet
|
||||||
|
motor.env = reading(24, 40, true, 0, 0, now);
|
||||||
|
auto v = s.sample();
|
||||||
|
REQUIRE(v);
|
||||||
|
CHECK(v->temp_c == doctest::Approx(24.0));
|
||||||
|
CHECK(v->humidity_pct == doctest::Approx(40.0));
|
||||||
|
CHECK(s.connected());
|
||||||
|
CHECK(s.name() == "DHT11");
|
||||||
|
|
||||||
|
SUBCASE("a failed read is not served as a reading") {
|
||||||
|
motor.env = reading(24, 40, false, 255, 0, now);
|
||||||
|
CHECK_FALSE(s.sample());
|
||||||
|
}
|
||||||
|
SUBCASE("a line older than 2.5 read periods is stale") {
|
||||||
|
now += McuEnvSensor::kStaleMs + 1;
|
||||||
|
CHECK_FALSE(s.sample());
|
||||||
|
CHECK_FALSE(s.connected());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a fan driven hard with no rpm looks stalled; idle or turning does not") {
|
||||||
|
CHECK(fanLooksStalled(reading(30, 40, true, 200, 0, 0)));
|
||||||
|
CHECK(fanLooksStalled(reading(0, 0, false, 255, 0, 0))); // the failsafe, not turning
|
||||||
|
CHECK_FALSE(fanLooksStalled(reading(20, 40, true, 0, 0, 0))); // off, as it should be
|
||||||
|
CHECK_FALSE(fanLooksStalled(reading(30, 40, true, 100, 0, 0))); // too slow to be sure
|
||||||
|
CHECK_FALSE(fanLooksStalled(reading(30, 40, true, 200, 900, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("hardware.ini: cooling needs the DHT11, and only 'fan' or 'none'") {
|
||||||
|
const std::string cfg_path = "test_mcuenv_config.ini";
|
||||||
|
const std::string hw_path = "test_mcuenv_hardware.ini";
|
||||||
|
{
|
||||||
|
std::ofstream c(cfg_path);
|
||||||
|
c << "[General]\ntower_name = Legacy\n[Camera.rgb]\nid = X\nprofile_dir = /tmp/p\n";
|
||||||
|
}
|
||||||
|
auto write_hw = [&](const std::string& hardware, const std::string& pins) {
|
||||||
|
std::ofstream h(hw_path);
|
||||||
|
h << "[Board]\nmodel = lattepanda-3-delta\nfirmware_env = leonardo\n"
|
||||||
|
"[Hardware]\naxes = yaw\nimu = none\ncameras = rgb\n" << hardware <<
|
||||||
|
"[Pins]\nyaw_cs = 8\nyaw_en = 7\n" << pins;
|
||||||
|
};
|
||||||
|
|
||||||
|
SUBCASE("a legacy tower loads, with its MCU pins") {
|
||||||
|
write_hw("env = dht11\ncooling = fan\n",
|
||||||
|
"dht = 9\nfan_pwm = 10\nfan_tach = 11\nspi_idle_cs = 2\n");
|
||||||
|
auto c = ConfigLoader::loadFromFile(cfg_path, hw_path);
|
||||||
|
CHECK(c.hardware.env == "dht11");
|
||||||
|
CHECK(c.hardware.cooling == "fan");
|
||||||
|
CHECK(c.hardware.pins.dht == 9);
|
||||||
|
CHECK(c.hardware.pins.fan_tach == 11);
|
||||||
|
CHECK(c.hardware.pins.spi_idle_cs == 2);
|
||||||
|
}
|
||||||
|
SUBCASE("a fan without the DHT11 is refused") {
|
||||||
|
write_hw("env = sht41\ncooling = fan\n", "fan_pwm = 10\n");
|
||||||
|
CHECK_THROWS_WITH_AS(ConfigLoader::loadFromFile(cfg_path, hw_path),
|
||||||
|
doctest::Contains("needs Hardware.env = dht11"), std::runtime_error);
|
||||||
|
}
|
||||||
|
SUBCASE("an unknown cooling is refused") {
|
||||||
|
write_hw("env = dht11\ncooling = water\n", "dht = 9\n");
|
||||||
|
CHECK_THROWS_WITH_AS(ConfigLoader::loadFromFile(cfg_path, hw_path),
|
||||||
|
doctest::Contains("'none' or 'fan'"), std::runtime_error);
|
||||||
|
}
|
||||||
|
SUBCASE("towers without it are unchanged") {
|
||||||
|
write_hw("env = sht41\n", "");
|
||||||
|
auto c = ConfigLoader::loadFromFile(cfg_path, hw_path);
|
||||||
|
CHECK(c.hardware.cooling.empty());
|
||||||
|
CHECK(c.hardware.pins.dht == -1);
|
||||||
|
}
|
||||||
|
std::remove(cfg_path.c_str());
|
||||||
|
std::remove(hw_path.c_str());
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue