Pages

Monday, April 12, 2010

Accessing an Arduino from Erlang

(2013-01-08: There is a native Erlang interface to Unix serial ports here: srly. See the readme for the API. It should work on Mac OS X, BSD and Linux.
And, if you want to load code to the Arduino directly from Erlang, try this: stk500)

First we'll need to upload a sketch to the Arduino. There are many examples floating around. Here is a simple one to control LED's. Yes, not exactly novel.
#define DEBUG 0
#define FPIN 8 // first Pin
#define LPIN 13 // last Pin
void setup() {
int i = 0;
Serial.begin(9600);
for (i = FPIN; i <= LPIN; i++)
pinMode(i, OUTPUT);
}
void loop() {
int val = 0;
int inb = 0;
if (Serial.available() > 0) {
inb = Serial.read();
#ifdef DEBUG
// for debugging from the serial monitor
if ( (inb >= '0') && (inb <= '9'))
inb -= '0';
#endif /* DEBUG */
for (val = 0; val <= LPIN - FPIN; val++)
digitalWrite(val + FPIN, ( (inb & (1 << val)) ? HIGH : LOW));
}
}
view raw LED.pde hosted with ❤ by GitHub

You will need to modify the definitions of FPIN and LPIN to match the first and last digital pins the LED's are plugged into. Compile and upload the sketch.

The lights are set as a bitmask. For example, to activate the first LED, send the integer 1; for the third LED, send 4; and for both the first and third LED, send 5. If you defined the DEBUG macro, you can test from the serial monitor by sending the ASCII representation of the numbers.

Now download and compile the erlang-serial port driver. Start up Erlang:
1> Pid = serial:start([{open, "/dev/ttyUSB0"}, {speed, 9600}]).
<0.47.0>
2> Pid ! {send, 1 bsl 2}. % LED at position 3

Doing Something Useful


Switching LED's on and off programmatically is hours of fun. But, like many others have discovered before, hook it up to a monitoring system and it becomes sort of useful.

I have a monitoring service running Nagios. Yeah, I think Nagios sucks too. The Erlang code does a request for the tactical overview page, parses out the statistics and sets the alert status accordingly.
-module(health).
-behaviour(gen_server).
-export([start_link/0,
check/0, alert/0, dump/0,
threshold/1, poll/1, url/1
]).
-export([init/1, handle_call/3]).
-define(SERVER, ?MODULE).
-define(URL, "http://YOUR_HOSTNAME_HERE/nagios/cgi-bin/tac.cgi").
-define(LED, [
{critical, 1 bsl 2},
{warning, 1 bsl 3},
{unknown, 1 bsl 3},
{ok, 1 bsl 4},
{error, (1 bsl 5) - 1}
]
).
-record(threshold, {
critical = 0,
warning = 0,
unknown = 0
}).
-record(status, {
critical = 0,
warning = 0,
unknown = 0,
ok = 0
}).
-record(state, {
poll = false,
url = ?URL,
interval = 60 * 1000, % 1 minute
threshold = #threshold{},
serial,
current = ok,
status = #status{}
}).
alert() ->
gen_server:call(?SERVER, alert).
check() ->
gen_server:call(?SERVER, check).
dump() ->
gen_server:call(?SERVER, dump).
poll(true) ->
gen_server:call(?SERVER, {poll, true}),
health:check(),
health:alert();
poll(false) ->
gen_server:call(?SERVER, {poll, false}).
threshold(N) when tuple_size(N) =:= 3 ->
gen_server:call(?SERVER, #threshold{
critical = element(1,N),
warning = element(2,N),
unknown = element(3,N)
}).
url(N) when is_list(N) ->
gen_server:call(?SERVER, {url, N}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
init([]) ->
inets:start(),
Pid = serial:start([{open, "/dev/ttyUSB0"}, {speed, 9600}]),
{ok, #state{serial = Pid, poll = true}}.
handle_call(check, _From, #state{interval = T} = State) ->
Status = try fetch(State) of
N ->
interval(State, ?SERVER, check),
N
catch
_:_ ->
interval(State#state{interval = T div 4}, ?SERVER, check),
error
end,
{reply, ok, State#state{status = Status}};
handle_call(alert, _From, State) ->
Status = alert(State),
led(Status, State),
interval(State, ?SERVER, alert),
{reply, ok, State#state{current = Status}};
handle_call(#threshold{} = N, _From, State) ->
{reply, ok, State#state{threshold = N}};
handle_call(dump, _From, State) ->
{reply, State, State};
handle_call({poll, N}, _From, State) ->
{reply, ok, State#state{poll = N}};
handle_call({url, N}, _From, State) ->
{reply, ok, State#state{url = N}}.
%%
%% Internal
%%
fetch(#state{url = URL}) ->
{ok, {{"HTTP/1.1",200,"OK"}, _Headers, Body}} = httpc:request(URL),
{match, Crit} = re:run(Body, "(\\d+)\\sCritical", [{capture, [1], binary}]),
{match, Warn} = re:run(Body, "(\\d+)\\sWarning", [{capture, [1], binary}]),
{match, Unk} = re:run(Body, "(\\d+)\\sUnknown", [{capture, [1], binary}]),
{match, Ok} = re:run(Body, "(\\d+)\\sOk", [{capture, [1], binary}]),
#status{
critical = b2i(Crit),
warning = b2i(Warn),
unknown = b2i(Unk),
ok = b2i(Ok)
}.
alert(#state{status = error}) ->
error;
alert(#state{status = S, threshold = T}) ->
Crit = S#status.critical,
Warn = S#status.warning,
Unk = S#status.unknown,
error_logger:info_report([
{crit, Crit, T#threshold.critical},
{warn, Warn, T#threshold.warning},
{unk, Unk, T#threshold.unknown}
]),
Status = try
threshold(critical, Crit, T#threshold.critical),
threshold(warning, Warn, T#threshold.warning),
threshold(unknown, Unk, T#threshold.unknown) of
_ -> ok
catch
throw:N -> N
end,
error_logger:info_report([{status, Status}]),
Status.
threshold(critical,N,T) when N > T -> throw(critical);
threshold(warning,N,T) when N > T -> throw(warning);
threshold(unknown,N,T) when N > T -> throw(unknown);
threshold(_,_,_) -> ok.
led(Status, #state{current = Status}) ->
ok;
led(Status, #state{serial = Serial}) ->
error_logger:info_report([{led, proplists:get_value(Status, ?LED)}]),
Serial ! {send, proplists:get_value(Status, ?LED)},
ok.
b2i([N]) when is_binary(N) ->
list_to_integer(binary_to_list(N)).
interval(State, M, F) ->
interval(State, M, F, []).
interval(#state{poll = true, interval = T}, M, F, A) ->
timer:apply_after(T, M, F, A);
interval(_, _, _, _) ->
ok.
view raw health.erl hosted with ❤ by GitHub

Monitoring Ambient Light


Here is another simple project: creating a graph of ambient light levels using an LDR and the really quite awesome eplot library.

Connect the LDR to an analog pin and upload a sketch:
int LDR = 2;
int val = 0;
int inb = 0;
void setup() {
Serial.begin(9600);
pinMode(LDR, INPUT);
}
void loop() {
if (Serial.available() > 0) {
inb = Serial.read();
if (inb == '1') {
val = analogRead(LDR);
//Serial.println(val);
Serial.print(val);
}
}
}
view raw LDR.pde hosted with ❤ by GitHub
The Erlang code has 2 components: a process that periodically reads and stores the sensor data and a web server that displays a graph in PNG format.
light:start().
sensor:start().
Reading the sensor:
-module(light).
-export([start/0,start/1]).
-define(TABLE, sensor).
-define(STORE, "db/sensor.db").
-define(TTY, "/dev/ttyUSB0").
-define(LDR, $1).
-define(INTRVL, 1000).
start() ->
start([]).
start(Options) ->
dets:open_file(?TABLE, [{type, set}, {file, ?STORE}]),
TTY = proplists:get_value(tty, Options, ?TTY),
Interval = proplists:get_value(interval, Options, ?INTRVL),
Speed = proplists:get_value(speed, Options, 9600),
LDR = proplists:get_value(ldr, Options, ?LDR),
Pid = serial:start([{open, TTY}, {speed, Speed}]),
timer:send_interval(Interval, Pid, {send, LDR}),
loop(Pid).
loop(Pid) ->
receive
{data, N} when is_binary(N) ->
%io:format("~w~n", [binary_to_integer(N)]),
dets:insert(?TABLE, {erlang:now(), binary_to_integer(N)}),
loop(Pid);
Error ->
io:format("error:~p~n", [Error])
end.
binary_to_integer(B) when is_binary(B) ->
list_to_integer(binary_to_list(B)).
view raw light.erl hosted with ❤ by GitHub

Displaying the graph:
-module(sensor).
-include("light.hrl").
-export([start/0,stop/0]).
% web interface
-export([graph/3]).
start() ->
dets:open_file(?TABLE, [{file, ?STORE}]),
application:start(inets),
inets:start(httpd, [
{modules, [
mod_alias,
mod_auth,
mod_esi,
mod_actions,
mod_cgi,
mod_dir,
mod_get,
mod_head,
mod_log,
mod_disk_log
]},
{port, 8889},
{server_name, "foo"},
{server_root, "log"},
{document_root, "www"},
{directory_index, ["index.html"]},
{error_log, "error.log"},
{security_log, "security.log"},
{transfer_log, "transfer.log"},
{erl_script_alias, {"/web", [sensor]}}
]).
stop() ->
inets:stop().
%%%
%%% web services
%%%
graph(SessionID, _Env, _Input) ->
mod_esi:deliver(SessionID, header(png)),
B = generate_graph(),
mod_esi:deliver(SessionID, binary_to_list(B)).
%%%
%%% helper functions
%%%
generate_graph() ->
L = get_values(),
egd_chart:graph([{"light",L}], [
{bg_rgba, {255,255,255,255}},
{width, 1024},
{height, 800}
]).
header(png) ->
"Content-Type: image/png\r\n\r\n".
get_values() ->
L = lists:sort([
{calendar:datetime_to_gregorian_seconds(calendar:now_to_universal_time(K)), V}
|| [{K, V}] <- dets:match(sensor, '$1') ]),
[ {K - element(1,hd(L)), V} || {K,V} <- L ].
view raw sensor.erl hosted with ❤ by GitHub

After the web server is running, the web page can be found by going to:
http://localhost:8889/web/sensor:graph

(I think the spikiness was caused by a blinking LED somewhere around the LDR).

1 comment:

Note: Only a member of this blog may post a comment.