Home Posts Tags Post Search Tag Search

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.