Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/zigbee_home/firmware/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func parseConfigFile(configPath string) (*config.Device, error) {

conf, err := config.ParseFromFile(absConfigPath)
if err != nil {
return nil, fmt.Errorf("parse config file: %w", err)
return nil, fmt.Errorf("parse config file at %q: %w", absConfigPath, err)
}

return conf, nil
Expand Down
2 changes: 2 additions & 0 deletions config/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ func ValidateConfiguration(cfg *Device) error {
}
}

// TODO: Add some recursive validation.

return nil
}

Expand Down
2 changes: 1 addition & 1 deletion examples/sensor_bme280/zigbee.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ sensors:
# I2C instance ID. Should be defined in Zephyr's board definition.
id: i2c0
# Default address for BME280.
addr: '0x76'
addr: 0x76
5 changes: 5 additions & 0 deletions examples/sensor_ina2xx/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## TI INA2XX voltage, current and power sensor

Simple example with one TI INA226 sensor attached and using I2C interface.

This example should also work with other INA22X and INA23X chips as well.
44 changes: 44 additions & 0 deletions examples/sensor_ina2xx/zigbee.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
general:
board: nrf52840dongle_nrf52840

board:
i2c:
# ID of instance is the same as defined in the SoC definition.
# Generally they are in form of `i2c[0-9]`.
# Number of i2c instances is also limited, and this allows only to
# re-define pins for specified i2c instance.
- id: i2c0
# Pins can also be defined, if want to re-bind I2C to another ones.
# sda: 0.29
# scl:
# port: 0
# pin: 31

sensors:
- type: ina2xx
# Must specify which model of sensor is being used.
# Note that currently only INA226 was tested,
# but other INA22X and INA23X should work as well.
model: ina226
# We also need to tell which I2C bus to use.
# It must be defined in board.i2c configuration,
# as shown above.
i2c:
id: i2c0
# This is optional configuration.
# It tells how many measurements sensor will
# use to average and return as result.
# Can be from 1 to 1024, as a power of 2 (so 1, 2, 4, 8, 16, ...).
avg: 4
# Reuiqred configuration.
# Value in milliohms of shunt resistor used together
# with INA2XX. Please be sure that the value is correct,
# otherwise current and power values would be wrong.
#
# Value 2 in this example means 0.002 Ohm == 2 mOhm.
shunt_resistance_mohms: 2
# This value is required (maybe for now).
# It specifies maximum expected current,
# in Amps, that circuit would measure.
# Generally it defines the step of the sensor.
max_current_amps: 10
18 changes: 6 additions & 12 deletions sensor/base/types.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
package base

import "strings"
import (
"fmt"
)

type I2CConnection struct {
ID string
Addr string
Addr uint8
}

func (c I2CConnection) UnitAddress() string {
if strings.HasPrefix(c.Addr, "0x") {
return c.Addr[2:]
}

return c.Addr
return fmt.Sprintf("%x", c.Addr)
}

func (c I2CConnection) Reg() string {
if !strings.HasPrefix(c.Addr, "0x") {
return "0x" + c.Addr
}

return c.Addr
return fmt.Sprintf("%#x", c.Addr)
}
2 changes: 1 addition & 1 deletion sensor/sensirion/scd4x.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (s SCD4X) ApplyOverlay(tree *dt.DeviceTree) error {
return dt.ErrNodeNotFound(s.I2C.ID)
}
// SCD4X address is static
s.I2C.Addr = "0x62"
s.I2C.Addr = 0x62

i2cNode.AddNodes(&dt.Node{
Name: "scd4x",
Expand Down
153 changes: 153 additions & 0 deletions sensor/ti/ina2xx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package ti

import (
"fmt"
"math"
"math/bits"
"strconv"
"strings"

"github.com/ffenix113/zigbee_home/sensor/base"
"github.com/ffenix113/zigbee_home/templates/extenders"
dt "github.com/ffenix113/zigbee_home/types/devicetree"
"github.com/ffenix113/zigbee_home/types/generator"
"github.com/ffenix113/zigbee_home/zcl/cluster"
)

type INA2XX struct {
*base.Base `yaml:",inline"`

Model string

I2C base.I2CConnection

// Max current to be measured in Amps.
MaxCurrentAmps float32 `yaml:"max_current_amps"`
// Shunt resistance in milli Ohms.
ShuntResistanceMohms float32 `yaml:"shunt_resistance_mohms"`
// Number of samples to average.
Avg uint16
}

func (i *INA2XX) String() string {
return fmt.Sprintf("INA2XX (%q)", strings.ToLower(i.Model))
}

func (*INA2XX) CPPComponentType() string {
return "BasicSensor"
}

func (*INA2XX) Clusters() cluster.Clusters {
return []cluster.Cluster{
cluster.ElectricalMeasurement{},
}
}

func (*INA2XX) NeedsDevice() bool {
return true
}

func (i *INA2XX) ApplyOverlay(overlay *dt.DeviceTree) error {
if i.Model == "" {
return fmt.Errorf("model cannot be empty")
}

i.Model = strings.ToLower(i.Model)

if i.I2C.ID == "" {
return fmt.Errorf("i2c id cannot be empty")
}

if i.ShuntResistanceMohms <= 0 {
return fmt.Errorf("shunt resister value cannot be less or equal to 0")
}

if i.MaxCurrentAmps <= 0 {
return fmt.Errorf("max current cannot be less or equal to 0")
}

if i.Avg == 0 {
i.Avg = 1
}

if i.Avg > 1024 {
i.Avg = 1024
}

if bits.OnesCount16(i.Avg) != 1 {
return fmt.Errorf("avg count should be power of two, but was %d", i.Avg)
}

aliases := overlay.FindSpecificNode(dt.SearchByName(dt.NodeNameRoot), dt.SearchByName(dt.NodeNameAliases))
aliases.AddProperties(dt.NewProperty(i.DeviceTreeLabel(), dt.Label(i.Label())))

i2c := overlay.FindSpecificNode(dt.SearchByLabel(i.I2C.ID))
if i2c == nil {
return fmt.Errorf("i2c bus with id %q should be already set up", i.I2C.ID)
}

if i.I2C.Addr == 0 {
// Default for ina226.
// Yes, this may be wrong for other models or configurations.
i.I2C.Addr = 0x40
}

// TODO: verify this value
shuntMicroOhmsStr := strconv.Itoa(int(i.ShuntResistanceMohms * 1_000))
// TODO: verify this value
currentMicroAmpsLsb := int(i.MaxCurrentAmps * 1_000_000 / (math.MaxInt16 + 1))

// 1 lsb step in microvolts.
LsbUvForModel := map[string]float32{
"ina226": 2.5, // 2.5 uV
"ina228": 0.3125, // 312.5 nV, ADCRANGE = 0
"ina230": 2.5, // 2.5 uV
"ina232": 2.5, // 2.5 uV, ADCRANGE = 0
"ina236": 2.5, // 2.5 uV, ADCRANGE = 0
"ina237": 5, // 5 uV, ADCRANGE = 0
}

uVForModel := LsbUvForModel[i.Model]
if uVForModel == 0 {
return fmt.Errorf("unsupported ina2xx model to fetch min uV per LSB: %q", i.Model)
}

// Verify that we did not fall through the floor of possible value of the chip.
// lsbUv / Rshunt > currentMicroAmpsLSB
// 10^-6 / 10^-3 == 10^-3
//
// This calculation is done explicitely, without simplifying
// so it is a bit more explicit.
minAmps := (uVForModel / 1_000_000) / (i.ShuntResistanceMohms / 1_000)
if minMicroAmpsLsb := 1_000_000 * minAmps; minMicroAmpsLsb > float32(currentMicroAmpsLsb) {
currentMicroAmpsLsb = int(minMicroAmpsLsb)
}

currentMicroAmpsStr := strconv.Itoa(currentMicroAmpsLsb)

inaNode := &dt.Node{
Name: i.Model,
Label: i.Label(),
UnitAddress: i.I2C.UnitAddress(),
Properties: []dt.Property{
dt.PropertyStatusEnable,
dt.NewProperty(dt.PropertyNameCompatible, dt.Quoted("ti,"+i.Model)),

dt.NewProperty("reg", dt.Angled(dt.String(i.I2C.Reg()))),
dt.NewProperty("avg-count", dt.FromValue(i.Avg)),
dt.NewProperty("rshunt-micro-ohms", dt.Angled(dt.String(shuntMicroOhmsStr))),
dt.NewProperty("current-lsb-microamps", dt.Angled(dt.String(currentMicroAmpsStr))),
},
}

i2c.AddNodes(inaNode)

return nil
}

func (*INA2XX) Extenders() []generator.Extender {
return []generator.Extender{
extenders.NewSensor(),
extenders.ElectricalMeasurement{},
}
}
31 changes: 31 additions & 0 deletions templates/extenders/electrical_measurement.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package extenders

import (
"github.com/ffenix113/zigbee_home/types/generator"
)

var _ generator.Extender = ElectricalMeasurement{}

type ElectricalMeasurement struct {
generator.SimpleExtender
}

func (l ElectricalMeasurement) WriteFiles() []generator.WriteFile {
return []generator.WriteFile{
{
FileName: "cluster_electrical_measurement.hpp",
TemplateName: "cluster_electrical_measurement.hpp",
},
{
FileName: "cluster_electrical_measurement.cpp",
TemplateName: "cluster_electrical_measurement.cpp",
},
}
}

func (l ElectricalMeasurement) Includes() []string {
return []string{
"cluster_electrical_measurement.hpp",
"zbhome_sensor.hpp",
}
}
38 changes: 30 additions & 8 deletions templates/extenders/i2c.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,15 @@ type I2C struct {

func NewI2C(instances ...I2CInstance) I2C {
for i, instance := range instances {
if len(instance.ID) != 4 || !strings.HasPrefix(instance.ID, "i2c") || (instance.ID[3] < '0' || instance.ID[3] > '9') {
panic(fmt.Sprintf("i2c instance %d must have `id` format of 'i2c[0-9]'", i))
validLength := len(instance.ID) == 4 || len(instance.ID) == 5
hasPrefix := strings.HasPrefix(instance.ID, "i2c")
validNum := (instance.ID[3] >= '0' || instance.ID[3] <= '9')
if len(instance.ID) == 5 {
validNum = validNum && (instance.ID[4] >= '0' || instance.ID[4] <= '9')
}

if !validLength || !hasPrefix || !validNum {
panic(fmt.Sprintf("i2c instance %d must have `id` format of 'i2c[0-9]' or 'i2c[0-9][0-9]'", i))
}
}

Expand All @@ -41,17 +48,32 @@ func (i I2C) ApplyOverlay(dt *devicetree.DeviceTree) error {
pinctrl := dt.FindSpecificNode(devicetree.SearchByLabel(devicetree.NodeLabelPinctrl))

for _, instance := range i.Instances {
pinsDefined := instance.SDA.PinsDefined() && instance.SCL.PinsDefined()
// Add pin definitions only if we have some.
// Otherwise just enable the I2C instance.
if instance.SDA.PinsDefined() && instance.SCL.PinsDefined() {
if pinsDefined {
pinctrl.AddNodes(buildI2C(instance.ID, instance)...)
}

dt.AddNodes(&devicetree.Node{
Label: instance.ID,
Upsert: true,
Properties: []devicetree.Property{devicetree.PropertyStatusEnable},
})
pinctrlDefault := devicetree.Angled(devicetree.Label(instance.ID + "_default"))
pinctrlSleep := devicetree.Angled(devicetree.Label(instance.ID + "_sleep"))
pinctrlNames := devicetree.Array(devicetree.Quoted("default"), devicetree.Quoted("sleep"))

i2cNode := &devicetree.Node{
Label: instance.ID,
Upsert: true,
Properties: []devicetree.Property{
devicetree.PropertyStatusEnable,
},
}

if pinsDefined {
i2cNode.AddProperties(devicetree.NewProperty("pinctrl-0", pinctrlDefault),
devicetree.NewProperty("pinctrl-1", pinctrlSleep),
devicetree.NewProperty("pinctrl-names", pinctrlNames))
}

dt.AddNodes(i2cNode)
}

return nil
Expand Down
Loading