T1000e full sensors

This commit is contained in:
liquidraver
2026-08-21 20:28:03 +02:00
parent 1b12354b85
commit cb4863e4bd
10 changed files with 599 additions and 12 deletions
+4
View File
@@ -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
+20
View File
@@ -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
+37 -3
View File
@@ -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
+11 -1
View File
@@ -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);
+384
View File
@@ -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 */
+14 -4
View File
@@ -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
+3
View File
@@ -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();
+3
View File
@@ -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.