mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 19:38:20 +00:00
T1000e full sensors
This commit is contained in:
@@ -588,6 +588,10 @@ target_sources(app PRIVATE adapters/clock/ZephyrRTCDiscover.c)
|
||||
# goertek,spa06 node is present.
|
||||
target_sources_ifdef(CONFIG_SENSOR app PRIVATE adapters/sensors/spa06.c)
|
||||
|
||||
# T1000-E onboard NTC + photocell on the SAADC. Same deal: compiles to nothing
|
||||
# when no seeed,t1000e-analog node is present.
|
||||
target_sources_ifdef(CONFIG_SENSOR app PRIVATE adapters/sensors/t1000e_analog.c)
|
||||
|
||||
# ========== Battery Curve Selection ==========
|
||||
# helpers/battery_curve.c is always compiled — it provides battery_curve_lookup()
|
||||
# and the weak battery_curve_default. A board-specific battery_curve.c is compiled
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
/* LPP Type Codes (from CayenneLPP spec) */
|
||||
#define LPP_ANALOG_INPUT 2 /* 2 bytes, 0.01 signed */
|
||||
#define LPP_LUMINOSITY 101 /* 2 bytes, 1 lux unsigned */
|
||||
#define LPP_TEMPERATURE 103 /* 2 bytes, 0.1°C signed */
|
||||
#define LPP_RELATIVE_HUMIDITY 104 /* 1 byte, 0.5% unsigned */
|
||||
#define LPP_BAROMETRIC_PRESSURE 115 /* 2 bytes, 0.1 hPa unsigned */
|
||||
@@ -93,6 +94,25 @@ public:
|
||||
return addField1(channel, LPP_RELATIVE_HUMIDITY, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add luminosity reading
|
||||
*
|
||||
* The LPP type is nominally lux at 1-unit resolution, and a real
|
||||
* ambient-light part gives lux. Boards whose light sensor reports a
|
||||
* relative scale instead (the T1000-E photocell's 0-100) send that scale
|
||||
* through unchanged — matching what Arduino MeshCore reports there, so a
|
||||
* node reads the same after reflashing.
|
||||
*
|
||||
* @param channel Channel number
|
||||
* @param value Luminosity, clamped to the 16-bit field
|
||||
* @return Number of bytes written, or 0 on overflow
|
||||
*/
|
||||
uint8_t addLuminosity(uint8_t channel, float value) {
|
||||
if (value < 0.0f) value = 0.0f;
|
||||
if (value > 65535.0f) value = 65535.0f;
|
||||
return addField2Unsigned(channel, LPP_LUMINOSITY, (uint16_t)value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add barometric pressure reading
|
||||
* @param channel Channel number
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
*
|
||||
* Auto-detects available sensors via Zephyr devicetree nodelabels.
|
||||
*
|
||||
* Environment sensors (temp/humidity/pressure):
|
||||
* Environment sensors (temp/humidity/pressure/light):
|
||||
* SHTC3, AHT20/DHT20/AM2301B, SHT4x, SHT3xD, BME280, BME680, BMP280, BMP388, LPS22HB, SPA06
|
||||
* MCU die temperature as fallback (nordic,nrf-temp)
|
||||
* Board-local analog sensors (seeed,t1000e-analog: NTC thermistor + photocell)
|
||||
*
|
||||
* Power monitors (voltage/current/power):
|
||||
* INA219, INA3221, INA226, INA228, INA230, INA232, INA236, INA237
|
||||
@@ -48,6 +49,7 @@ LOG_MODULE_REGISTER(zephcore_sensors, CONFIG_ZEPHCORE_SENSORS_LOG_LEVEL);
|
||||
#if HAS_ENV_SENSORS
|
||||
static const struct device *temp_humidity_dev = NULL;
|
||||
static const struct device *pressure_dev = NULL;
|
||||
static const struct device *light_dev = NULL;
|
||||
static bool temp_dev_has_pressure = false; /* BME280/BME680 also have pressure */
|
||||
static bool env_available = false;
|
||||
|
||||
@@ -184,7 +186,20 @@ check_pressure:
|
||||
}
|
||||
|
||||
done:
|
||||
env_available = (temp_humidity_dev != NULL) || (pressure_dev != NULL);
|
||||
/* === Board-local analog sensors ===
|
||||
* Not on any bus — a thermistor and a photocell wired straight to the
|
||||
* SoC's ADC, so there is nothing to probe and the node's presence in DT
|
||||
* is the whole detection. Its thermistor is read last in
|
||||
* env_sensors_read() and only fills in a temperature nothing else
|
||||
* supplied — a dedicated part beats a thermistor inside the case. */
|
||||
dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(t1000e_sensors));
|
||||
if (sensor_ready(dev)) {
|
||||
light_dev = dev;
|
||||
LOG_INF("Found analog sensors: %s (T1000-E NTC + photocell)", dev->name);
|
||||
}
|
||||
|
||||
env_available = (temp_humidity_dev != NULL) || (pressure_dev != NULL) ||
|
||||
(light_dev != NULL);
|
||||
if (!env_available) {
|
||||
LOG_INF("No environment sensors found");
|
||||
}
|
||||
@@ -250,6 +265,25 @@ int env_sensors_read(struct env_data *data)
|
||||
}
|
||||
}
|
||||
|
||||
/* === Board-local analog sensors (light, and thermistor as fallback) ===
|
||||
* One fetch covers both channels — it switches the sensor rail, so
|
||||
* splitting it would pay that cost twice. */
|
||||
if (light_dev) {
|
||||
if (sensor_sample_fetch(light_dev) == 0) {
|
||||
if (sensor_channel_get(light_dev, SENSOR_CHAN_LIGHT, &val) == 0) {
|
||||
data->luminosity = sensor_value_to_float(&val);
|
||||
data->has_luminosity = true;
|
||||
}
|
||||
/* Only where no bus sensor — nor a barometer's die
|
||||
* channel above — already produced a temperature. */
|
||||
if (!data->has_temperature &&
|
||||
sensor_channel_get(light_dev, SENSOR_CHAN_AMBIENT_TEMP, &val) == 0) {
|
||||
data->temperature_c = sensor_value_to_float(&val);
|
||||
data->has_temperature = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* === MCU die temperature — always read when available ===
|
||||
* Used as fallback when no external temp sensor, and always
|
||||
* available via has_mcu_temperature for telemetry decisions. */
|
||||
@@ -263,7 +297,7 @@ int env_sensors_read(struct env_data *data)
|
||||
}
|
||||
|
||||
return (data->has_temperature || data->has_humidity || data->has_pressure ||
|
||||
data->has_mcu_temperature) ? 0 : -ENODATA;
|
||||
data->has_luminosity || data->has_mcu_temperature) ? 0 : -ENODATA;
|
||||
#else
|
||||
return -ENOTSUP;
|
||||
#endif
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
* SPDX-License-Identifier: MIT
|
||||
* Zephyr Environment & Power Sensors
|
||||
*
|
||||
* Environment: temperature, humidity, pressure
|
||||
* Environment: temperature, humidity, pressure, light
|
||||
* Supports: SHTC3, AHT20/DHT20/AM2301B, SHT4x, SHT3x, BME280, BME680, BMP280, BMP388, LPS22HB
|
||||
* MCU die temperature as fallback (nordic,nrf-temp)
|
||||
* Board-local analog sensors: T1000-E NTC thermistor + photocell
|
||||
*
|
||||
* Power monitors: voltage, current, power
|
||||
* Supports: INA219, INA3221, INA226, INA228, INA230, INA232, INA236, INA237
|
||||
@@ -27,12 +28,21 @@ struct env_data {
|
||||
float humidity_pct; /* Relative humidity in percent */
|
||||
float pressure_hpa; /* Barometric pressure in hPa */
|
||||
float mcu_temperature_c; /* MCU die temperature in Celsius */
|
||||
float luminosity; /* Ambient light — see note below */
|
||||
bool has_temperature;
|
||||
bool has_humidity;
|
||||
bool has_pressure;
|
||||
bool has_mcu_temperature;
|
||||
bool has_luminosity;
|
||||
};
|
||||
|
||||
/* Note on luminosity: the unit is whatever the board's light sensor reports on
|
||||
* SENSOR_CHAN_LIGHT, and it is forwarded to CayenneLPP luminosity unscaled.
|
||||
* A true ambient-light part gives lux; the T1000-E's photocell gives Seeed's
|
||||
* 0-100 scale, which Arduino MeshCore also reports verbatim. Keeping it
|
||||
* unscaled is what makes a ZephCore node read the same as the stock firmware
|
||||
* it replaced. */
|
||||
|
||||
/* Initialize environment sensors (call once at boot) */
|
||||
int env_sensors_init(void);
|
||||
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
/*
|
||||
* Seeed Tracker T1000-E onboard analog sensors — NTC thermistor + photocell
|
||||
* Copyright (c) 2026 ZephCore
|
||||
* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Ported from Arduino MeshCore variants/t1000-e/t1000e_sensors.cpp, which in
|
||||
* turn carries Seeed's own conversions. Both are reproduced here so a node
|
||||
* reports the same numbers it did on the stock firmware.
|
||||
*/
|
||||
|
||||
#define DT_DRV_COMPAT seeed_t1000e_analog
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/adc.h>
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
#include <zephyr/drivers/regulator.h>
|
||||
#include <zephyr/drivers/sensor.h>
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/logging/log.h>
|
||||
|
||||
#if DT_HAS_COMPAT_STATUS_OKAY(DT_DRV_COMPAT)
|
||||
|
||||
LOG_MODULE_REGISTER(t1000e_analog, CONFIG_SENSOR_LOG_LEVEL);
|
||||
|
||||
/* Averaged per channel. The parts are slow and the rail is already paying a
|
||||
* 10 ms settle, so a few extra conversions are free noise rejection. */
|
||||
#define T1000E_ADC_SAMPLES 4
|
||||
|
||||
/* ================================================================
|
||||
* NTC thermistor
|
||||
*
|
||||
* Resistance in ohms at each degree from -30 C (index 0) to +105 C
|
||||
* (index 135) — a 10k-at-25C part, effective beta about 3250. Seeed's
|
||||
* firmware carries a parallel array of temperatures, which is just the
|
||||
* index minus 30, so only the resistances are stored here.
|
||||
*
|
||||
* (Seeed's source also defines a beta of 4250 next to this table. It is
|
||||
* dead code there — nothing reads it — and it does not describe this
|
||||
* curve, so do not "simplify" the table into a beta formula with it.)
|
||||
* ================================================================ */
|
||||
|
||||
#define NTC_TABLE_LEN 136
|
||||
#define NTC_TABLE_T_MIN (-30)
|
||||
|
||||
static const uint32_t ntc_res[NTC_TABLE_LEN] = {
|
||||
113347, 107565, 102116, 96978, 92132, 87559, 83242, 79166, 75316, 71677,
|
||||
68237, 64991, 61919, 59011, 56258, 53650, 51178, 48835, 46613, 44506,
|
||||
42506, 40600, 38791, 37073, 35442, 33892, 32420, 31020, 29689, 28423,
|
||||
27219, 26076, 24988, 23951, 22963, 22021, 21123, 20267, 19450, 18670,
|
||||
17926, 17214, 16534, 15886, 15266, 14674, 14108, 13566, 13049, 12554,
|
||||
12081, 11628, 11195, 10780, 10382, 10000, 9634, 9284, 8947, 8624,
|
||||
8315, 8018, 7734, 7461, 7199, 6948, 6707, 6475, 6253, 6039,
|
||||
5834, 5636, 5445, 5262, 5086, 4917, 4754, 4597, 4446, 4301,
|
||||
4161, 4026, 3896, 3771, 3651, 3535, 3423, 3315, 3211, 3111,
|
||||
3014, 2922, 2834, 2748, 2666, 2586, 2509, 2435, 2364, 2294,
|
||||
2228, 2163, 2100, 2040, 1981, 1925, 1870, 1817, 1766, 1716,
|
||||
1669, 1622, 1578, 1535, 1493, 1452, 1413, 1375, 1338, 1303,
|
||||
1268, 1234, 1202, 1170, 1139, 1110, 1081, 1053, 1026, 999,
|
||||
974, 949, 925, 902, 880, 858,
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
* Photocell
|
||||
*
|
||||
* Seeed maps the divider voltage onto 0-100 with a dead band at each end.
|
||||
* LIGHT_SPAN_MV is deliberately not (LIGHT_MAX_MV - LIGHT_MIN_MV): the
|
||||
* stock firmware divides the 80..2480 mV range by 2400 while subtracting
|
||||
* only the 80 mV floor, and the top of the range is clamped rather than
|
||||
* reached. Reproduced as-is so readings match.
|
||||
* ================================================================ */
|
||||
|
||||
#define LIGHT_MIN_MV 80
|
||||
#define LIGHT_MAX_MV 2480
|
||||
#define LIGHT_SPAN_MV 2400
|
||||
|
||||
struct t1000e_config {
|
||||
struct adc_dt_spec ntc;
|
||||
struct adc_dt_spec light;
|
||||
struct adc_dt_spec vcc;
|
||||
struct gpio_dt_spec power_gpio;
|
||||
const struct device *power_supply;
|
||||
uint32_t vcc_mv_multiplier;
|
||||
uint32_t vcc_max_mv;
|
||||
uint32_t ntc_series_ohms;
|
||||
uint16_t settle_time_ms;
|
||||
};
|
||||
|
||||
struct t1000e_data {
|
||||
float temperature_c;
|
||||
float light_pct;
|
||||
bool temperature_valid;
|
||||
bool light_valid;
|
||||
};
|
||||
|
||||
/* Averaged raw reading for one channel, or a negative errno. */
|
||||
static int t1000e_read_raw(const struct adc_dt_spec *spec)
|
||||
{
|
||||
int32_t total = 0;
|
||||
int valid = 0;
|
||||
|
||||
for (int i = 0; i < T1000E_ADC_SAMPLES; i++) {
|
||||
int16_t sample = 0;
|
||||
struct adc_sequence seq = {
|
||||
.buffer = &sample,
|
||||
.buffer_size = sizeof(sample),
|
||||
};
|
||||
|
||||
if (adc_sequence_init_dt(spec, &seq) < 0) {
|
||||
continue;
|
||||
}
|
||||
if (adc_read_dt(spec, &seq) == 0) {
|
||||
total += sample;
|
||||
valid++;
|
||||
}
|
||||
}
|
||||
|
||||
if (valid == 0) {
|
||||
return -EIO;
|
||||
}
|
||||
|
||||
total /= valid;
|
||||
|
||||
/* A divider cannot swing below the rail's ground; a negative code is
|
||||
* SAADC offset noise around zero, and would invert the conversions. */
|
||||
return (total < 0) ? 0 : (int)total;
|
||||
}
|
||||
|
||||
/* Millivolts at the pin, or a negative errno. */
|
||||
static int t1000e_read_mv(const struct adc_dt_spec *spec)
|
||||
{
|
||||
int32_t mv = t1000e_read_raw(spec);
|
||||
|
||||
if (mv < 0) {
|
||||
return mv;
|
||||
}
|
||||
if (adc_raw_to_millivolts_dt(spec, &mv) < 0) {
|
||||
return -EINVAL;
|
||||
}
|
||||
return (int)mv;
|
||||
}
|
||||
|
||||
static float t1000e_ntc_temperature(const struct t1000e_config *cfg,
|
||||
uint32_t vcc_mv, uint32_t ntc_mv)
|
||||
{
|
||||
float rp = (float)cfg->ntc_series_ohms;
|
||||
float rt;
|
||||
int i;
|
||||
|
||||
/* Divider is rail - NTC - node - Rp - ground, so
|
||||
* Vnode = Vcc * Rp / (Rp + Rntc) => Rntc = Rp * (Vcc - Vnode) / Vnode.
|
||||
* A zero reading means an open NTC or an unpowered rail: infinitely
|
||||
* cold on this curve, which the table floor turns into its low clamp. */
|
||||
if (ntc_mv == 0) {
|
||||
return (float)NTC_TABLE_T_MIN;
|
||||
}
|
||||
|
||||
rt = rp * ((float)vcc_mv / (float)ntc_mv - 1.0f);
|
||||
|
||||
/* Table is descending, so the first entry the resistance reaches or
|
||||
* exceeds bounds it from above. */
|
||||
for (i = 0; i < NTC_TABLE_LEN; i++) {
|
||||
if (rt >= (float)ntc_res[i]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Off either end of the curve. Seeed's loop indexes out of bounds in
|
||||
* both of these cases; clamp instead. */
|
||||
if (i == 0) {
|
||||
return (float)NTC_TABLE_T_MIN;
|
||||
}
|
||||
if (i == NTC_TABLE_LEN) {
|
||||
return (float)(NTC_TABLE_T_MIN + NTC_TABLE_LEN - 1);
|
||||
}
|
||||
|
||||
/* Linear interpolation between the bracketing entries, which are
|
||||
* exactly one degree apart. The 0.05 is Seeed's rounding compensation:
|
||||
* every consumer downstream truncates to a tenth of a degree. */
|
||||
return (float)(NTC_TABLE_T_MIN + i - 1) +
|
||||
((float)ntc_res[i - 1] - rt) /
|
||||
(float)(ntc_res[i - 1] - ntc_res[i]) +
|
||||
0.05f;
|
||||
}
|
||||
|
||||
static float t1000e_light_percent(uint32_t light_mv)
|
||||
{
|
||||
if (light_mv <= LIGHT_MIN_MV) {
|
||||
return 0.0f;
|
||||
}
|
||||
if (light_mv >= LIGHT_MAX_MV) {
|
||||
return 100.0f;
|
||||
}
|
||||
return 100.0f * (float)(light_mv - LIGHT_MIN_MV) / (float)LIGHT_SPAN_MV;
|
||||
}
|
||||
|
||||
static int t1000e_power(const struct t1000e_config *cfg, bool on)
|
||||
{
|
||||
int rc = 0;
|
||||
|
||||
if (on) {
|
||||
if (cfg->power_supply != NULL) {
|
||||
rc = regulator_enable(cfg->power_supply);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
if (cfg->power_gpio.port != NULL) {
|
||||
rc = gpio_pin_set_dt(&cfg->power_gpio, 1);
|
||||
if (rc < 0) {
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
k_msleep(cfg->settle_time_ms);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (cfg->power_gpio.port != NULL) {
|
||||
(void)gpio_pin_set_dt(&cfg->power_gpio, 0);
|
||||
}
|
||||
if (cfg->power_supply != NULL) {
|
||||
(void)regulator_disable(cfg->power_supply);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int t1000e_sample_fetch(const struct device *dev,
|
||||
enum sensor_channel chan)
|
||||
{
|
||||
const struct t1000e_config *cfg = dev->config;
|
||||
struct t1000e_data *data = dev->data;
|
||||
int ntc_mv, light_mv, vcc_raw;
|
||||
uint32_t vcc_mv;
|
||||
int rc;
|
||||
|
||||
if (chan != SENSOR_CHAN_ALL && chan != SENSOR_CHAN_AMBIENT_TEMP &&
|
||||
chan != SENSOR_CHAN_LIGHT) {
|
||||
return -ENOTSUP;
|
||||
}
|
||||
|
||||
rc = t1000e_power(cfg, true);
|
||||
if (rc < 0) {
|
||||
LOG_ERR("sensor rail power-up failed: %d", rc);
|
||||
(void)t1000e_power(cfg, false);
|
||||
return rc;
|
||||
}
|
||||
|
||||
ntc_mv = t1000e_read_mv(&cfg->ntc);
|
||||
light_mv = t1000e_read_mv(&cfg->light);
|
||||
vcc_raw = t1000e_read_raw(&cfg->vcc);
|
||||
|
||||
(void)t1000e_power(cfg, false);
|
||||
|
||||
/* The rail divider carries its own scaling, so it goes through the
|
||||
* board's multiplier rather than the generic raw-to-millivolts helper. */
|
||||
if (vcc_raw < 0) {
|
||||
vcc_mv = cfg->vcc_max_mv;
|
||||
} else {
|
||||
vcc_mv = ((uint32_t)vcc_raw * cfg->vcc_mv_multiplier) / 4096u;
|
||||
if (vcc_mv > cfg->vcc_max_mv) {
|
||||
vcc_mv = cfg->vcc_max_mv;
|
||||
}
|
||||
}
|
||||
|
||||
data->temperature_valid = (ntc_mv >= 0);
|
||||
if (data->temperature_valid) {
|
||||
data->temperature_c =
|
||||
t1000e_ntc_temperature(cfg, vcc_mv, (uint32_t)ntc_mv);
|
||||
}
|
||||
|
||||
data->light_valid = (light_mv >= 0);
|
||||
if (data->light_valid) {
|
||||
data->light_pct = t1000e_light_percent((uint32_t)light_mv);
|
||||
}
|
||||
|
||||
/* Millidegrees rather than a split integer/fraction pair: the naive
|
||||
* split prints "-11.-75" below freezing, which is exactly where these
|
||||
* readings most need checking. */
|
||||
LOG_DBG("ntc=%dmV light=%dmV vcc=%umV -> %d m°C, %d%%",
|
||||
ntc_mv, light_mv, vcc_mv,
|
||||
(int)(data->temperature_c * 1000.0f),
|
||||
(int)data->light_pct);
|
||||
|
||||
if (!data->temperature_valid && !data->light_valid) {
|
||||
return -EIO;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int t1000e_channel_get(const struct device *dev,
|
||||
enum sensor_channel chan,
|
||||
struct sensor_value *val)
|
||||
{
|
||||
struct t1000e_data *data = dev->data;
|
||||
|
||||
switch (chan) {
|
||||
case SENSOR_CHAN_AMBIENT_TEMP:
|
||||
if (!data->temperature_valid) {
|
||||
return -ENODATA;
|
||||
}
|
||||
return sensor_value_from_float(val, data->temperature_c);
|
||||
|
||||
case SENSOR_CHAN_LIGHT:
|
||||
if (!data->light_valid) {
|
||||
return -ENODATA;
|
||||
}
|
||||
return sensor_value_from_float(val, data->light_pct);
|
||||
|
||||
default:
|
||||
return -ENOTSUP;
|
||||
}
|
||||
}
|
||||
|
||||
static DEVICE_API(sensor, t1000e_api) = {
|
||||
.sample_fetch = t1000e_sample_fetch,
|
||||
.channel_get = t1000e_channel_get,
|
||||
};
|
||||
|
||||
static int t1000e_init(const struct device *dev)
|
||||
{
|
||||
const struct t1000e_config *cfg = dev->config;
|
||||
const struct adc_dt_spec *chans[] = { &cfg->ntc, &cfg->light, &cfg->vcc };
|
||||
int rc;
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(chans); i++) {
|
||||
if (!adc_is_ready_dt(chans[i])) {
|
||||
LOG_ERR("ADC %s not ready", chans[i]->dev->name);
|
||||
return -ENODEV;
|
||||
}
|
||||
rc = adc_channel_setup_dt(chans[i]);
|
||||
if (rc < 0) {
|
||||
LOG_ERR("ADC channel %u setup failed: %d",
|
||||
chans[i]->channel_id, rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg->power_supply != NULL && !device_is_ready(cfg->power_supply)) {
|
||||
LOG_ERR("sensor rail regulator not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
|
||||
if (cfg->power_gpio.port != NULL) {
|
||||
if (!gpio_is_ready_dt(&cfg->power_gpio)) {
|
||||
LOG_ERR("sensor enable GPIO not ready");
|
||||
return -ENODEV;
|
||||
}
|
||||
rc = gpio_pin_configure_dt(&cfg->power_gpio, GPIO_OUTPUT_INACTIVE);
|
||||
if (rc < 0) {
|
||||
LOG_ERR("sensor enable GPIO config failed: %d", rc);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INF("T1000-E analog sensors ready (NTC + photocell)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define T1000E_POWER_SUPPLY(inst) \
|
||||
COND_CODE_1(DT_INST_NODE_HAS_PROP(inst, power_supply), \
|
||||
(DEVICE_DT_GET(DT_INST_PHANDLE(inst, power_supply))), \
|
||||
(NULL))
|
||||
|
||||
#define T1000E_DEFINE(inst) \
|
||||
static struct t1000e_data t1000e_data_##inst; \
|
||||
static const struct t1000e_config t1000e_config_##inst = { \
|
||||
.ntc = ADC_DT_SPEC_INST_GET_BY_NAME(inst, ntc), \
|
||||
.light = ADC_DT_SPEC_INST_GET_BY_NAME(inst, light), \
|
||||
.vcc = ADC_DT_SPEC_INST_GET_BY_NAME(inst, vcc), \
|
||||
.power_gpio = GPIO_DT_SPEC_INST_GET_OR(inst, power_gpios, {0}),\
|
||||
.power_supply = T1000E_POWER_SUPPLY(inst), \
|
||||
.vcc_mv_multiplier = DT_INST_PROP(inst, vcc_mv_multiplier), \
|
||||
.vcc_max_mv = DT_INST_PROP(inst, vcc_max_mv), \
|
||||
.ntc_series_ohms = DT_INST_PROP(inst, ntc_series_ohms), \
|
||||
.settle_time_ms = DT_INST_PROP(inst, settle_time_ms), \
|
||||
}; \
|
||||
SENSOR_DEVICE_DT_INST_DEFINE(inst, t1000e_init, NULL, \
|
||||
&t1000e_data_##inst, \
|
||||
&t1000e_config_##inst, \
|
||||
POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY,\
|
||||
&t1000e_api);
|
||||
|
||||
DT_INST_FOREACH_STATUS_OKAY(T1000E_DEFINE)
|
||||
|
||||
#endif /* DT_HAS_COMPAT_STATUS_OKAY */
|
||||
@@ -1223,7 +1223,7 @@ bool CompanionMesh::vcontactHandleFrame(const uint8_t *data, size_t len)
|
||||
if (tag == 0) tag = 1;
|
||||
sendPacketSent(MSG_SEND_SENT_DIRECT, tag, 3000);
|
||||
|
||||
uint8_t rsp[8 + 4 + 11 + 11 + (12 * POWER_MAX_CHANNELS) + 8];
|
||||
uint8_t rsp[8 + 4 + 11 + 15 + (12 * POWER_MAX_CHANNELS) + 8];
|
||||
int i = 0;
|
||||
rsp[i++] = PUSH_CODE_TELEMETRY_RESPONSE;
|
||||
rsp[i++] = 0; /* reserved */
|
||||
@@ -1565,6 +1565,16 @@ int CompanionMesh::appendSelfTelemetry(uint8_t *reply, uint8_t permissions)
|
||||
reply[i++] = (press >> 8) & 0xFF;
|
||||
reply[i++] = press & 0xFF;
|
||||
}
|
||||
if (env.has_luminosity) {
|
||||
reply[i++] = CH_SELF;
|
||||
reply[i++] = LPP_LUMINOSITY;
|
||||
float lum = env.luminosity;
|
||||
if (lum < 0.0f) lum = 0.0f;
|
||||
if (lum > 65535.0f) lum = 65535.0f;
|
||||
uint16_t lux = (uint16_t)lum;
|
||||
reply[i++] = (lux >> 8) & 0xFF;
|
||||
reply[i++] = lux & 0xFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Power monitor telemetry (INA219/INA3221/ina2xx)
|
||||
@@ -3152,9 +3162,9 @@ bool CompanionMesh::handleProtocolFrame(const uint8_t *data, size_t len)
|
||||
// Response: [PUSH_CODE_TELEMETRY_RESPONSE][reserved][6-byte pubkey][telemetry_data]
|
||||
// Worst-case size tracks POWER_MAX_CHANNELS so a future bump can't
|
||||
// silently overflow this stack buffer. With current value 4:
|
||||
// header(8) + batt(4) + gps(11) + env(temp4+hum3+press4=11)
|
||||
// + power(POWER_MAX_CHANNELS * 12 = 48) + 8 byte safety pad = 90.
|
||||
uint8_t rsp[8 + 4 + 11 + 11 + (12 * POWER_MAX_CHANNELS) + 8];
|
||||
// header(8) + batt(4) + gps(11) + env(temp4+hum3+press4+lum4=15)
|
||||
// + power(POWER_MAX_CHANNELS * 12 = 48) + 8 byte safety pad = 94.
|
||||
uint8_t rsp[8 + 4 + 11 + 15 + (12 * POWER_MAX_CHANNELS) + 8];
|
||||
int i = 0;
|
||||
rsp[i++] = PUSH_CODE_TELEMETRY_RESPONSE;
|
||||
rsp[i++] = 0; // reserved
|
||||
|
||||
@@ -391,6 +391,9 @@ int RepeaterMesh::handleRequest(ClientInfo* sender, uint32_t sender_timestamp, u
|
||||
if (env.has_pressure) {
|
||||
lpp.addBarometricPressure(CH_SELF, env.pressure_hpa);
|
||||
}
|
||||
if (env.has_luminosity) {
|
||||
lpp.addLuminosity(CH_SELF, env.luminosity);
|
||||
}
|
||||
} else {
|
||||
/* No env sensors at all — try MCU temp directly */
|
||||
float mcu_temp = _board.getMCUTemperature();
|
||||
|
||||
@@ -156,6 +156,9 @@ int RoomServerMesh::handleRequest(ClientInfo* sender, uint32_t sender_timestamp,
|
||||
if (env.has_pressure) {
|
||||
lpp.addBarometricPressure(CH_SELF, env.pressure_hpa);
|
||||
}
|
||||
if (env.has_luminosity) {
|
||||
lpp.addLuminosity(CH_SELF, env.luminosity);
|
||||
}
|
||||
} else {
|
||||
/* No env sensors at all — try MCU temp directly */
|
||||
float mcu_temp = _board.getMCUTemperature();
|
||||
|
||||
@@ -105,6 +105,29 @@
|
||||
vbat-mv-multiplier = <7236>; /* 2:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
|
||||
/* Onboard NTC thermistor (AIN7) and photocell (AIN5).
|
||||
*
|
||||
* Both dividers hang off the sensor_power rail AND need SENSOR_EN
|
||||
* (P0.4) asserted — Arduino drives both before every read. The driver
|
||||
* switches them per sample fetch; regulator refcounting keeps that
|
||||
* safe alongside the battery read, which shares the rail.
|
||||
*
|
||||
* The NTC conversion needs the rail voltage feeding its divider, so
|
||||
* the battery channel is wired in here as "vcc" with the same
|
||||
* multiplier zephyr,user uses. It is clamped to vcc-max-mv, which is
|
||||
* where it sits for all but the flattest cell. */
|
||||
t1000e_sensors: analog-sensors {
|
||||
compatible = "seeed,t1000e-analog";
|
||||
io-channels = <&adc 7>, <&adc 5>, <&adc 0>;
|
||||
io-channel-names = "ntc", "light", "vcc";
|
||||
vcc-mv-multiplier = <7236>;
|
||||
vcc-max-mv = <3300>;
|
||||
ntc-series-ohms = <8250>;
|
||||
settle-time-ms = <10>;
|
||||
power-gpios = <&gpio0 4 GPIO_ACTIVE_HIGH>;
|
||||
power-supply = <&sensor_power>;
|
||||
};
|
||||
|
||||
/* Buzzer on PWM0 channel 0 (P0.25) with enable on P1.05 */
|
||||
pwmbuzzer {
|
||||
compatible = "pwm-leds";
|
||||
@@ -227,7 +250,24 @@
|
||||
};
|
||||
};
|
||||
|
||||
/* I2C0 for QMA6100P accelerometer + external sensors */
|
||||
/* I2C0 — QMA6100P accelerometer only, and it is not declared here.
|
||||
*
|
||||
* Deliberately does NOT include common/sensors-i2c.dtsi. This is a sealed
|
||||
* tracker: there is no Grove/expansion header, so none of those parts can
|
||||
* ever be attached, and Arduino MeshCore agrees — the t1000-e variant uses
|
||||
* its own T1000SensorManager with no I2C environment sensors at all, unlike
|
||||
* the boards that pull in EnvironmentSensorManager.
|
||||
*
|
||||
* Including the list cost ~3.5 s of every boot. Both rails feeding this bus
|
||||
* (sensor_power on P1.6, and the accelerometer's own P1.7) are off at boot,
|
||||
* so its pull-ups are unpowered and SDA/SCL sit low. A transfer to a dead
|
||||
* bus never completes, so each of the seven phantom parts burned a full
|
||||
* CONFIG_I2C_NRFX_TRANSFER_TIMEOUT (500 ms) instead of NAKing in
|
||||
* microseconds the way it would on a live bus.
|
||||
*
|
||||
* The bus stays enabled so the accelerometer can be added as a single node
|
||||
* later — but powering P1.7 is part of that change, not optional.
|
||||
*/
|
||||
&i2c0 {
|
||||
compatible = "nordic,nrf-twim";
|
||||
status = "okay";
|
||||
@@ -235,9 +275,6 @@
|
||||
pinctrl-0 = <&i2c0_default>;
|
||||
pinctrl-1 = <&i2c0_sleep>;
|
||||
pinctrl-names = "default", "sleep";
|
||||
|
||||
/* All supported environment & power sensors — auto-detected at runtime */
|
||||
#include "../../common/sensors-i2c.dtsi"
|
||||
};
|
||||
|
||||
/* SPI1 for LR1110 */
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Copyright (c) 2026 ZephCore
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
description: |
|
||||
Seeed Tracker T1000-E onboard analog sensors.
|
||||
|
||||
Two passive parts sit on the board's switched 3V3 sensor rail and are read
|
||||
through the SoC's ADC:
|
||||
|
||||
- An NTC thermistor forming the upper leg of a divider, reported as
|
||||
SENSOR_CHAN_AMBIENT_TEMP in degrees C. The resistance is converted
|
||||
through the same resistance/temperature table Seeed's own firmware uses,
|
||||
so readings match the stock and Arduino MeshCore builds.
|
||||
|
||||
- A photocell, reported as SENSOR_CHAN_LIGHT. Seeed's firmware maps it to
|
||||
a 0-100 scale rather than to lux, and Arduino MeshCore forwards that
|
||||
scale verbatim as CayenneLPP luminosity, so this driver does the same.
|
||||
The value is a percentage despite the channel's nominal lux units.
|
||||
|
||||
Neither divider outputs a valid voltage until the rail regulator and the
|
||||
sensor-enable GPIO are both asserted and given time to settle, so the driver
|
||||
switches them around each sample fetch rather than leaving them on.
|
||||
|
||||
compatible: "seeed,t1000e-analog"
|
||||
|
||||
include: [sensor-device.yaml]
|
||||
|
||||
properties:
|
||||
io-channels:
|
||||
required: true
|
||||
description: |
|
||||
ADC channels, in the order named by io-channel-names: "ntc", "light" and
|
||||
"vcc". "vcc" is the battery/rail divider channel — converting the NTC
|
||||
divider needs the rail voltage feeding it, not a constant.
|
||||
|
||||
io-channel-names:
|
||||
required: true
|
||||
|
||||
vcc-mv-multiplier:
|
||||
type: int
|
||||
required: true
|
||||
description: |
|
||||
Scales the raw "vcc" reading to millivolts as mv = raw * multiplier / 4096.
|
||||
Same convention as the zephyr,user vbat-mv-multiplier property, and
|
||||
normally the same value, since it is normally the same channel.
|
||||
|
||||
vcc-max-mv:
|
||||
type: int
|
||||
default: 3300
|
||||
description: |
|
||||
Rail voltage ceiling in millivolts, used to clamp the measured "vcc"
|
||||
before it becomes the NTC divider reference. Above the regulator's
|
||||
dropout the divider is fed from the fixed rail rather than from the
|
||||
cell, so the higher battery reading would overstate it. Only a cell
|
||||
flat enough to drag the rail down reads below this.
|
||||
|
||||
power-gpios:
|
||||
type: phandle-array
|
||||
description: |
|
||||
Sensor-enable pin (SENSOR_EN, P0.4 on the T1000-E), asserted for the
|
||||
duration of a sample fetch. This is in addition to power-supply: the
|
||||
T1000-E gates these two sensors behind both.
|
||||
|
||||
power-supply:
|
||||
type: phandle
|
||||
description: |
|
||||
Regulator for the switched sensor rail (PIN_3V3_EN, P1.6 on the
|
||||
T1000-E), enabled for the duration of a sample fetch. Refcounted, so it
|
||||
nests safely with the battery read, which switches the same rail.
|
||||
|
||||
settle-time-ms:
|
||||
type: int
|
||||
default: 10
|
||||
description: |
|
||||
Delay between asserting the rail and enable pin and sampling, to let the
|
||||
divider and its decoupling capacitor settle.
|
||||
|
||||
ntc-series-ohms:
|
||||
type: int
|
||||
default: 8250
|
||||
description: |
|
||||
Series resistor in the lower leg of the NTC divider, in ohms.
|
||||
Reference in New Issue
Block a user