local base = _G local cjson = require "cjson.safe" local glob = require "posix.glob" local io = require("io") local string = require("string") local time = require "posix.time" local unistd = require "posix.unistd" local _M = {} local _internal_ = {} _internal_.setup = {} _internal_.read = {} -- ############# utils ########### function _internal_.write_to_file(path, data) -- base.print(string.format("writing '%s' to '%s'", data, path)) local f, err = io.open(path, "w") if not f then return nil, err end local ret, err = f:write(data) f:close() return ret, err end function _internal_.read_from_file(path) local f, err = io.open(path, "r") if not f then return nil, err end local data, err = f:read("*a") f:close() return data, err end -- ############# i2c ########### function _M.i2c_bus_path(bus) return string.format("/sys/bus/i2c/devices/i2c-%d", bus) end function _M.i2c_device_name(bus, address) return string.format("%d-%04x", bus, address) end function _M.i2c_device_path(bus, address) return string.format("/sys/bus/i2c/devices/" .. _M.i2c_device_name(bus, address)) end function _M.i2c_add_device(bus, address, name) return _internal_.write_to_file(_M.i2c_bus_path(bus) .. "/new_device", string.format("%s 0x%02x", name, address)) end function _M.i2c_delete_device(bus, address) return _internal_.write_to_file(_M.i2c_bus_path(bus) .. "/delete_device", string.format("0x%02x", address)) end function _M.i2c_get_bus_number_from_name(name) local bus_paths, glob_result = glob.glob("/sys/bus/i2c/devices/i2c-*", 0) if not bus_paths then if glob_result == glob.GLOB_NOMATCH then return {} end if glob_result == glob.GLOB_ABORTED then return nil, "glob(): aborted" end if glob_result == glob.GLOB_NOSPACE then return nil, "glob(): no space" end return nil, "glob(): unknown error" end for _, bus_path in base.pairs(bus_paths) do local num = string.match(bus_path, '/i2c%-(%d+)$') if not num then return nil, "unable to parse bus number from path: " .. bus_path end local contents, err = _internal_.read_from_file(bus_path .. "/name") if contents ~= nil and contents:gsub("%s+", "") == name then return num end end return nil, "unable to find i2c bus with name: " .. name end function _M.i2c_get_mux_channels(parent, address) local channel_paths, glob_result = glob.glob(_M.i2c_device_path(parent, address) .. "/channel-*", 0) if not channel_paths then if glob_result == glob.GLOB_NOMATCH then return {} end if glob_result == glob.GLOB_ABORTED then return nil, "glob(): aborted" end if glob_result == glob.GLOB_NOSPACE then return nil, "glob(): no space" end return nil, "glob(): unknown error" end local channels = {} for _, channel_path in base.pairs(channel_paths) do local channel = string.match(channel_path, '/channel%-(%d+)$') if not channel then return nil, "unable to parse channel number from path: " .. channel_path end local bus_path, err = unistd.readlink(channel_path) if not bus_path then return nil, err end local bus = string.match(bus_path, '/i2c%-(%d+)$') if not bus then return nil, "unable to parse bus number from path: " .. bus_path end channels[base.tonumber(channel)] = base.tonumber(bus) end return channels end function _M.i2c_add_devices_recursive(bus_num, bus_name, devices) if bus_num == nil then local err bus_num, err = _M.i2c_get_bus_number_from_name(bus_name) if bus_num == nil then return nil, err end end local num_sensors = 0 local device for _, device in base.ipairs(devices) do local ret, err = _M.i2c_add_device(bus_num, device.address, device.type) if not ret then return nil, err end local setup_function = _internal_.setup[device.type] if setup_function ~= nil then time.nanosleep({tv_sec = 0, tv_nsec = 100000000}) local ret, err = setup_function(bus_num, device.address) if ret == nil then return nil, err end num_sensors = num_sensors + ret end if device.channels then time.nanosleep({tv_sec = 0, tv_nsec = 100000000}) local mux_channels, err = _M.i2c_get_mux_channels(bus_num, device.address) if not mux_channels then return nil, error end local channel for _, channel in base.ipairs(device.channels) do if not mux_channels[channel.number] then return nil, string.format("i2c-mux %s has no channel %d", _M.i2c_device_name(bus_num, device.address), channel.number) end local tmp, err = _M.i2c_add_devices_recursive(mux_channels[channel.number], nil, channel.devices) if ret == nil then return nil, err end num_sensors = num_sensors + tmp end end end return num_sensors end function _M.i2c_read_sensors_recursive(bus_num, bus_name, devices) local readings = {} if bus_num == nil then local err bus_num, err = _M.i2c_get_bus_number_from_name(bus_name) if bus_num == nil then return readings end end local device for _, device in base.ipairs(devices) do local name = device.name ~= nil and device.name or device.type .. "_" .. _M.i2c_device_name(bus_num, device.address) local read_function = _internal_.read[device.type] if read_function ~= nil then local values, kind = read_function(bus_num, device.address) if values ~= nil then readings[name] = values readings[name]['_kind_'] = kind end end if device.channels then local mux_channels, err = _M.i2c_get_mux_channels(bus_num, device.address) if not mux_channels then return nil, error end local channel for _, channel in base.ipairs(device.channels) do if mux_channels[channel.number] then local tmp, err = _M.i2c_read_sensors_recursive(mux_channels[channel.number], nil, channel.devices) if tmp ~= nil then for k,v in base.pairs(tmp) do readings[k] = v end end end end end end return readings end -- ############# 1-wire ########### function _M.w1_device_path(address) return string.format("/sys/bus/w1/devices/" .. address) end function _M.w1_device_family_and_serial(address) local family, serial = string.match(address, '^(%x+)%-(%x+)$') if not family then return nil, "unable to extract device family and serial from 1-wire address " .. address end return family, serial end function _M.w1_setup_devices(devices) local num_sensors = 0 local device for _, device in base.ipairs(devices) do local family, _ = _M.w1_device_family_and_serial(device.address) if family ~= nil then local setup_function = _internal_.setup["w1-" .. family] if setup_function ~= nil then local tmp, err = setup_function(device.address) if tmp == nil then return nil, err end num_sensors = num_sensors + tmp end end end return num_sensors end function _M.w1_read_sensors(devices) local readings = {} local device for _, device in base.ipairs(devices) do local name = device.name ~= nil and device.name or device.address local family, _ = _M.w1_device_family_and_serial(device.address) if family ~= nil then local read_function = _internal_.read["w1-" .. family] if read_function ~= nil then local values, kind = read_function(device.address) if values ~= nil then readings[name] = values readings[name]['_kind_'] = kind end end end end return readings end -- ############# gpio ########### function _M.gpio_pin_path(number) return string.format("/sys/class/gpio/gpio%d", number) end function _M.gpio_export_pin(number) return _internal_.write_to_file("/sys/class/gpio/export", string.format("%d", number)) end function _M.gpio_unexport_pin(number) return _internal_.write_to_file("/sys/class/gpio/unexport", string.format("%d", number)) end function _M.gpio_read_pin(number) local value, err = _internal_.read_from_file(_M.gpio_pin_path(number) .. "/value") if not value then return nil, err end return value:match( "^%s*(.-)%s*$" ) end function _M.gpio_setup_pins(pins) local num_pins = 0 local pin for _, pin in base.ipairs(pins) do local ret, err = _M.gpio_export_pin(pin.number) if ret == nil then return nil, err end num_pins = num_pins + 1 end return num_pins end function _M.gpio_read_pins(pins) local readings = {} local pin for _, pin in base.ipairs(pins) do local name = pin.name ~= nil and pin.name or string.format("gpio-%d", pin.number) local value, err = _M.gpio_read_pin(pin.number) if value ~= nil then readings[name] = { gpio = value, _kind_ = 'gpio' } end end return readings end -- ############# iio ########### function _M.iio_device_path_from_i2c_device(bus, address) local device_paths, glob_result = glob.glob(_M.i2c_device_path(bus, address) .. "/iio:device*", 0) if not device_paths then if glob_result == glob.GLOB_NOMATCH then return nil, "couldn't find iio device handle for i2c device " .. _M.i2c_device_name(bus, address) end if glob_result == glob.GLOB_ABORTED then return nil, "glob(): aborted" end if glob_result == glob.GLOB_NOSPACE then return nil, "glob(): no space" end return nil, "glob(): unknown error" end if #device_paths ~= 1 then return nil, "found more than one iio device for i2c device " .. _M.i2c_device_name(bus, address) end return device_paths[1] end function _M.iio_setup_bmp280(bus, address) local iio_device, err = _M.iio_device_path_from_i2c_device(bus, address) if not iio_device then return nil, err end local ret, err = _internal_.write_to_file(iio_device .. '/in_pressure_oversampling_ratio', '1') if not ret then return nil, err end local ret, err = _internal_.write_to_file(iio_device .. '/in_temp_oversampling_ratio', '1') if not ret then return nil, err end return 2 end _internal_.setup['bmp280'] = _M.iio_setup_bmp280 function _M.iio_read_bmp280(bus, address) local iio_device, err = _M.iio_device_path_from_i2c_device(bus, address) if not iio_device then return end local values = {} local tmp, err = _internal_.read_from_file(iio_device .. '/in_pressure_input') if tmp ~= nil then local val = base.tonumber(tmp) if val ~= nil then values['pressure'] = val*1000 end end tmp, err = _internal_.read_from_file(iio_device .. '/in_temp_input') if tmp ~= nil then local val = base.tonumber(tmp) if val ~= nil then values['temperature'] = val/1000 end end return values, 'bmp280' end _internal_.read['bmp280'] = _M.iio_read_bmp280 function _M.iio_setup_bme280(bus, address) local iio_device, err = _M.iio_device_path_from_i2c_device(bus, address) if not iio_device then return nil, err end local ret, err = _internal_.write_to_file(iio_device .. '/in_humidityrelative_oversampling_ratio', '1') if not ret then return nil, err end local ret, err = _internal_.write_to_file(iio_device .. '/in_pressure_oversampling_ratio', '1') if not ret then return nil, err end local ret, err = _internal_.write_to_file(iio_device .. '/in_temp_oversampling_ratio', '1') if not ret then return nil, err end return 3 end _internal_.setup['bme280'] = _M.iio_setup_bme280 function _M.iio_read_bme280(bus, address) local iio_device, err = _M.iio_device_path_from_i2c_device(bus, address) if not iio_device then return end local values = {} local tmp, err = _internal_.read_from_file(iio_device .. '/in_humidityrelative_input') if tmp ~= nil then local val = base.tonumber(tmp) if val ~= nil then values['humidity'] = val/1000 end end local tmp, err = _internal_.read_from_file(iio_device .. '/in_pressure_input') if tmp ~= nil then local val = base.tonumber(tmp) if val ~= nil then values['pressure'] = val*1000 end end tmp, err = _internal_.read_from_file(iio_device .. '/in_temp_input') if tmp ~= nil then local val = base.tonumber(tmp) if val ~= nil then values['temperature'] = val/1000 end end return values, 'bme280' end _internal_.read['bme280'] = _M.iio_read_bme280 function _M.iio_setup_am2315(bus, address) return 2 end _internal_.setup['am2315'] = _M.iio_setup_am2315 function _M.iio_read_am2315(bus, address) local iio_device, err = _M.iio_device_path_from_i2c_device(bus, address) if not iio_device then return end local values = {} local tmp, err = _internal_.read_from_file(iio_device .. '/in_humidityrelative_raw') if tmp ~= nil then local val = base.tonumber(tmp) if val ~= nil then values['humidity'] = val/10 end end time.nanosleep({tv_sec = 0, tv_nsec = 100000000}) tmp, err = _internal_.read_from_file(iio_device .. '/in_temp_raw') if tmp ~= nil then local val = base.tonumber(tmp) if val ~= nil then values['temperature'] = val/10 end end return values, 'am2315' end _internal_.read['am2315'] = _M.iio_read_am2315 -- ############# hwmon ########### function _M.hwmon_device_path_from_w1_device(address) local device_paths, glob_result = glob.glob(_M.w1_device_path(address) .. "/hwmon/hwmon*", 0) if not device_paths then if glob_result == glob.GLOB_NOMATCH then return nil, "couldn't find hwmon device handle for 1-wire device " .. address end if glob_result == glob.GLOB_ABORTED then return nil, "glob(): aborted" end if glob_result == glob.GLOB_NOSPACE then return nil, "glob(): no space" end return nil, "glob(): unknown error" end if #device_paths ~= 1 then return nil, "found more than one hwmon device for 1-wire device " .. address end return device_paths[1] end function _M.hwmon_setup_w1_therm(address) return 1 end _internal_.setup['w1-10'] = _M.hwmon_setup_w1_therm _internal_.setup['w1-22'] = _M.hwmon_setup_w1_therm _internal_.setup['w1-28'] = _M.hwmon_setup_w1_therm _internal_.setup['w1-3B'] = _M.hwmon_setup_w1_therm _internal_.setup['w1-42'] = _M.hwmon_setup_w1_therm function _M.hwmon_read_w1_therm(address) local hwmon_device, err = _M.hwmon_device_path_from_w1_device(address) if not hwmon_device then return end local values = {} local tmp, err = _internal_.read_from_file(hwmon_device .. '/temp1_input') if tmp ~= nil then local val = base.tonumber(tmp) if val ~= nil then values['temperature'] = val/1000 end end return values end _internal_.read['w1-10'] = function(address) return _M.hwmon_read_w1_therm(address), "ds18s20" end _internal_.read['w1-22'] = function(address) return _M.hwmon_read_w1_therm(address), "ds1822" end _internal_.read['w1-28'] = function(address) return _M.hwmon_read_w1_therm(address), "ds18b20" end _internal_.read['w1-3B'] = function(address) return _M.hwmon_read_w1_therm(address), "ds1825" end _internal_.read['w1-42'] = function(address) return _M.hwmon_read_w1_therm(address), "ds28ea00" end -- ############# high-level interface ########### function _M.read_config(path) sensors_json, err = _internal_.read_from_file(path) if not sensors_json then return nil, err end return cjson.decode(sensors_json) end function _M.setup(config) local num_sensors = 0 if config.i2c then local i2c_bus for _, i2c_bus in base.ipairs(config.i2c) do local tmp, err = _M.i2c_add_devices_recursive(i2c_bus.number, i2c_bus.name, i2c_bus.devices) if tmp == nil then return nil, err end num_sensors = num_sensors + tmp end end if config.w1 then local tmp, err = _M.w1_setup_devices(config.w1) if tmp == nil then return nil, err end num_sensors = num_sensors + tmp end if config.gpio then local tmp, err = _M.gpio_setup_pins(config.gpio) if tmp == nil then return nil, err end num_sensors = num_sensors + tmp end return num_sensors end function _M.read(config) local readings = {} if config.i2c then local i2c_bus for _, i2c_bus in base.ipairs(config.i2c) do local tmp, err = _M.i2c_read_sensors_recursive(i2c_bus.number, i2c_bus.name, i2c_bus.devices) if tmp ~= nil then for k,v in base.pairs(tmp) do readings[k] = v end end end end if config.w1 then local tmp, err = _M.w1_read_sensors(config.w1) if tmp ~= nil then for k,v in base.pairs(tmp) do readings[k] = v end end end if config.gpio then local tmp, err = _M.gpio_read_pins(config.gpio) if tmp ~= nil then for k,v in base.pairs(tmp) do readings[k] = v end end end return readings end -- ################################ return _M