We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Post 59
Weather App 06 - LTR390-UV-1
Published on: 2025-08-13
Tags:
elixir, Blog, Side Project, Libraries, Nerves, Weather App, Poncho
This is the work I did to create the sensor package for the LTR390 UV sensor.
Include Dependencies for Sensors
Now we need to include the right dependencies for the other sensors that we have on the hat. In the book they talk about using some predefined libs :bmp280 and :sgp30 which will take some the needed functions in order to use the sensors. I might need to define my own but Ill see about using some libs if I can find them.
In order to make this work with the current settings I will write my own conn.ex, config.ex and sensor_name.ex files. Keep in mind you will need to add in the dependencies to the sensor hub after you make them in order to add them to the Supervisor tree.
conn.ex
This is where I took a lot of the existing code and added in some of the new addresses and made sure that I set all the correct values in the config.
@command_bit 0x80
@enable_register 0x00
@gain_register 0x05
@control_register 0x04
@als_data_register [0x0D, 0x0E, 0x0F]
@uvs_data_register [0x10, 0x11, 0x12]
def write_config(config, i2c, sensor) do
enable_byte = Config.to_enable_byte(config)
control_byte = Config.to_control_byte(config)
gain_byte = Config.to_gain_byte(config)
# Write ENABLE register (1 byte) with command bit set
I2C.write(i2c, sensor, <<@command_bit ||| @enable_register, enable_byte>>)
# Write CONTROL register (1 byte) with command bit set
I2C.write(i2c, sensor, <<@command_bit ||| @control_register, control_byte>>)
# Write the GAIN register
I2C.write(i2c, sensor, <<@command_bit ||| @gain_register, gain_byte>>)
end
The sensor for UV has two types of sensors so I needed to be sure that I set both of them and could read from either.
def read(i2c, sensor, %Config{uvs_als: :uvs} = config) do
<<low, mid, high>> =
I2C.write_read!(i2c, sensor, <<@command_bit ||| hd(@uvs_data_register)>>, 3)
raw = low ||| mid <<< 8 ||| high <<< 16
Config.uvs_to_uvi(config, raw)
end
def read(i2c, sensor, %Config{uvs_als: :als} = config) do
<<low, mid, high>> =
I2C.write_read!(i2c, sensor, <<@command_bit ||| hd(@als_data_register)>>, 3)
# Convert using ALS formula when you fix it
raw = low ||| mid <<< 8 ||| high <<< 16
Config.als_to_lux(config, raw, 0)
end
config.ex
This is where I had to set some new values for the struct and because there are some hardcoded formula that I would use for the conversion I set those values here too.
@uv_sensitivity_base 2300.0
defstruct resolution: :res_default,
measure_rate: :measure_rate_100_ms,
uvs_als: :uvs,
gain: :low,
reset: false
def new, do: struct(__MODULE__)
def new(opts) do
struct(__MODULE__, opts)
end
defp resolution(:res_20bit), do: 0b000
defp resolution(:res_19bit), do: 0b001
defp resolution(:res_18bit), do: 0b010
defp resolution(:res_17bit), do: 0b011
defp resolution(:res_16bit), do: 0b100
defp resolution(:res_15bit), do: 0b101
defp resolution(:res_default), do: 0b010
defp measure_rate(:measure_rate_25_ms), do: 0b000
defp measure_rate(:measure_rate_50_ms), do: 0b001
defp measure_rate(:measure_rate_100_ms), do: 0b010
defp measure_rate(:measure_rate_200_ms), do: 0b011
defp measure_rate(:measure_rate_500_ms), do: 0b100
defp measure_rate(:measure_rate_1000_ms), do: 0b101
defp measure_rate(:measure_rate_2000_ms), do: 0b110
defp measure_rate(:measure_rate_default), do: 0b010
defp gain(:min), do: 0b000
defp gain(:low), do: 0b001
defp gain(:med), do: 0b010
defp gain(:high), do: 0b011
defp gain(:max), do: 0b100
defp gain(:gain_default), do: 0b001
defp uvs_als(:uvs), do: 1
defp uvs_als(:als), do: 0
defp reset(true), do: 1
defp reset(_), do: 0
def to_control_byte(%__MODULE__{resolution: resolution, measure_rate: measure_rate}) do
resolution(resolution) <<< 4 ||| measure_rate(measure_rate)
end
def to_gain_byte(%__MODULE__{gain: gain}) do
gain(gain)
end
def to_enable_byte(%__MODULE__{uvs_als: uvs_als, reset: reset}) do
reset_bit_rate = reset(reset) <<< 4
uvs_als_bit_rate = uvs_als(uvs_als) <<< 3
enable_bit_rate = 1 <<< 1
reserve_bit_rate = 1 <<< 0
reset_bit_rate ||| uvs_als_bit_rate ||| enable_bit_rate |||
reserve_bit_rate
end
defp integration_time_ms(:measure_rate_25_ms), do: 25
defp integration_time_ms(:measure_rate_50_ms), do: 50
defp integration_time_ms(:measure_rate_100_ms), do: 100
defp integration_time_ms(:measure_rate_200_ms), do: 200
defp integration_time_ms(:measure_rate_500_ms), do: 500
defp integration_time_ms(:measure_rate_1000_ms), do: 1000
defp integration_time_ms(:measure_rate_2000_ms), do: 2000
defp integration_time_ms(:measure_rate_default), do: 100
def gain_multiplier(:min), do: 1
def gain_multiplier(:low), do: 3
def gain_multiplier(:med), do: 6
def gain_multiplier(:high), do: 9
def gain_multiplier(:max), do: 18
def als_to_lux(%{gain: gain, measure_rate: measure_rate}, raw_counts, wfac \\ 1.0) do
int_ms = integration_time_ms(measure_rate)
raw_counts * 0.6 / (gain_multiplier(gain) * (int_ms / 100)) * wfac
end
def uvs_to_uvi(%{gain: gain, measure_rate: measure_rate}, raw_counts, wfac \\ 1.0) do
int_ms = integration_time_ms(measure_rate)
sensitivity = @uv_sensitivity_base * (gain_multiplier(gain) / 18) * (int_ms / 400)
raw_counts / sensitivity * wfac
end
LTR390_UV.ex
Again very similar to the other sensor but we have to get 2 types of measurements so I had to implement those.
@impl true
def init(%{address: address, i2c_bus_name: bus_name} = args) do
i2c = Comm.open(bus_name)
config =
args
|> Map.take([:gain, :resolution, :uvs_als, :measure_rate, :reset])
|> Config.new()
Comm.write_config(config, i2c, address)
# Calculate measure rate in milliseconds (example mapping)
measure_rate =
case config.measure_rate do
:it_25_ms -> 25
:it_50_ms -> 50
:it_100_ms -> 100
:it_200_ms -> 200
:it_500_ms -> 500
:it_1000_ms -> 1000
:it_2000_ms -> 2000
_ -> 100
end
# Schedule first measure after measure_rate
Process.send_after(self(), :measure, measure_rate)
state = %{
i2c: i2c,
address: address,
config: config,
resolution: config.resolution,
measure_rate: measure_rate,
reset: config.reset,
gain: config.gain,
uvs_als: config.uvs_als,
last_uvs: :no_reading,
last_als: :no_reading
}
{:ok, state}
end
def init(args) do
{bus_name, address} = Comm.discover()
transport = "bus: #{bus_name}, address: #{address}"
Logger.info("Starting LTR390_UV. Please specify an address and a bus.")
Logger.info("Starting on " <> transport)
defaults =
args
|> Map.put(:address, address)
|> Map.put(:i2c_bus_name, bus_name)
init(defaults)
end
@doc """
This are set to take the last reading from the sensor, then update the reading with the new reading and
appending it to the state. Once that is done it will switch to the other type of sensor and schedule an other
reading.
"""
@impl true
def handle_info(
:measure,
%{
i2c: i2c,
address: address,
config: config,
measure_rate: measure_rate,
uvs_als: current_mode
} = state
) do
last_reading = Comm.read(i2c, address, config)
# Toggle mode and update config accordingly
{new_state, new_config} =
case current_mode do
:uvs ->
{%{state | last_uvs: last_reading, uvs_als: :als}, %{config | uvs_als: :als}}
:als ->
{%{state | last_als: last_reading, uvs_als: :uvs}, %{config | uvs_als: :uvs}}
end
# Write new config to switch sensor mode
Comm.write_config(new_config, i2c, address)
# Schedule next measurement after full measure_rate
Process.send_after(self(), :measure, measure_rate)
{:noreply, %{new_state | config: new_config}}
end
@doc """
This is called when the GenServer is asked to get the last measurements for both uvs and als.
"""
@impl true
def handle_call(:get_measurement, _from, state) do
last_uvs = state.last_uvs
last_als = state.last_als
{:reply, {last_uvs, last_als}, state}
end
@impl true
def terminate(_reason, %{i2c: i2c}) do
Circuits.I2C.close(i2c)
:ok
end
I plan on finishing all the other sensors at some point and will be adding in the libs to gihub after I finish each one.