-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathelixir_bme680.ex
More file actions
168 lines (146 loc) · 4.91 KB
/
elixir_bme680.ex
File metadata and controls
168 lines (146 loc) · 4.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
defmodule Bme680 do
@moduledoc """
Provides a high level abstraction to interface with the
BME680 environmental sensor on Linux platforms.
"""
use Bitwise
use GenServer
defmodule State do
@moduledoc false
defstruct port: nil, subscribers: [], async_subscribers: [], measuring: false
end
defmodule Measurement do
@moduledoc false
defstruct temperature: nil, pressure: nil, humidity: nil, gas_resistance: nil
end
@doc """
Starts and links the `Bme680` GenServer.
Options:
- `i2c_device_number` is the number of the i2c device, e.g. 1 for `/dev/i2c-1`
- `i2c_address` i2c address of the sensor. It can be only `0x76` or `0x77`
- `temperature_offset` is an offset, in degrees Celsius, that will be
subtracted to temperature measurements in order to compensate for the internal
heating of the device. It's typically around 4 or 5 degrees, and also
affects relative humidity calculations
"""
@spec start_link(
[
i2c_device_number: integer,
i2c_address: 0x76 | 0x77,
temperature_offset: non_neg_integer
],
[term]
) :: GenServer.on_start()
def start_link(bme_opts \\ [], opts \\ []) do
i2c_device_number = Keyword.get(bme_opts, :i2c_device_number, 1)
i2c_address = Keyword.get(bme_opts, :i2c_address, 0x76)
temperature_offset = Keyword.get(bme_opts, :temperature_offset, 0)
if Enum.member?([0x76, 0x77], i2c_address) do
arg = [i2c_device_number, i2c_address, temperature_offset]
GenServer.start_link(__MODULE__, arg, opts)
else
{:error, "invalid i2c address #{i2c_address}. Valid values are 0x76 and 0x77"}
end
end
@doc """
Perform a measurement on the BME680 sensor and synchronously return it
Measurements are structs like:
```
%Bme680.Measurement{
temperature: 21.74,
pressure: 1090.52,
humidity: 45.32,
gas_resistance: 10235
}
```
"""
@spec measure(GenServer.server()) :: %Measurement{
temperature: float,
pressure: float,
humidity: float,
gas_resistance: integer | nil
}
def measure(pid) do
GenServer.call(pid, :measure)
end
@doc """
Perform a measurement on the BME680 sensor and asynchronously send the result
as a message to the pid in `send_to`
"""
@spec measure_async(GenServer.server(), pid) :: :ok
def measure_async(pid, send_to) do
GenServer.cast(pid, {:measure_async, send_to})
end
@doc """
Gracefully stops the `Bme680` GenServer.
"""
@spec stop(GenServer.server()) :: :ok
def stop(pid) do
GenServer.cast(pid, :stop)
end
# GenServer callbacks
def init([i2c_device_number, i2c_address, temperature_offset]) do
executable_dir =
Application.get_env(:elixir_bme680, :executable_dir, :code.priv_dir(:elixir_bme680))
port =
Port.open({:spawn_executable, executable_dir ++ '/bme680'}, [
{:args, ["#{i2c_device_number}", "#{i2c_address}", "#{temperature_offset}"]},
{:line, 64},
:use_stdio,
:binary,
:exit_status
])
{:ok, %State{port: port, measuring: false, subscribers: []}}
end
def handle_call(
:measure,
from,
state = %State{port: port, subscribers: subscribers, measuring: measuring}
) do
unless measuring, do: Port.command(port, "measure\n")
{:noreply, %State{state | measuring: true, subscribers: [from | subscribers]}}
end
def handle_cast(
{:measure_async, pid},
state = %State{port: port, async_subscribers: subs, measuring: measuring}
) do
unless measuring, do: Port.command(port, "measure\n")
{:noreply, %State{state | measuring: true, async_subscribers: [pid | subs]}}
end
def handle_cast(:stop, state) do
{:stop, :normal, state}
end
def handle_info({p, {:data, {:eol, line}}}, %State{
port: p,
subscribers: subs,
async_subscribers: async_subs
}) do
measurement = decode_measurement(line)
for pid <- subs, do: GenServer.reply(pid, measurement)
for pid <- async_subs, do: send(pid, measurement)
{:noreply, %State{port: p, measuring: false, subscribers: [], async_subscribers: []}}
end
def handle_info({port, {:exit_status, exit_status}}, state = %State{port: port}) do
{:stop, exit_status, state}
end
# Private helper functions
defp decode_measurement(line) do
case line |> String.trim() |> String.split(",", trim: true) do
["T:" <> t, "P:" <> p, "H:" <> h, "G:" <> g] ->
%Measurement{
temperature: String.to_float(t),
pressure: String.to_float(p),
humidity: String.to_float(h),
gas_resistance: String.to_integer(g)
}
["T:" <> t, "P:" <> p, "H:" <> h] ->
%Measurement{
temperature: String.to_float(t),
pressure: String.to_float(p),
humidity: String.to_float(h)
}
_ ->
{:error, "Measurement failed"}
end
end
end