Skip to content

Commit

Permalink
Merge pull request #1 from rveshovda/simulator
Browse files Browse the repository at this point in the history
Simulator
  • Loading branch information
royveshovda authored Jul 26, 2016
2 parents ecd1eed + 4c1c861 commit 51c5993
Show file tree
Hide file tree
Showing 10 changed files with 388 additions and 47 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,30 @@ Pay attention to the has_fix if it is true or false. If has_fix=false, you canno
The most common usage pattern is to subscribe to the GenEvent publisher running
Check out the code inside the example-folder for an implementation for a subscriber. You need to implement (or copy) similar code to your side to receive new positions.

## TODO
## Usage: simulation

### Starting manually
Start a simulated port by calling the following:
```elixir
XGPS.Ports_supervisor.start_port(:simulate)
```


### Auto-start from config
By adding a line to config:
```elixir
config :xgps, port_to_start: {:simulate,}
```

### Sending simulated position
Send a simulated position using one of the following commands:
```elixir
XGPS.Ports_supervisor.send_simulated_no_fix()
XGPS.Ports_supervisor.send_simulated_position(1.1,2.2,3.3) # lat, lon, alt
```

## Future development
- Simulation reading from file
- Consider GenStage

## Note
Expand Down
1 change: 1 addition & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use Mix.Config
#config :xgps, port_to_start: {"/dev/serial0", :init_adafruit_gps}
#config :xgps, port_to_start: {:simulate,}
config :logger, level: :info

# It is also possible to import configuration files, relative to this
Expand Down
41 changes: 5 additions & 36 deletions lib/parser.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
defmodule XGPS.Parser do
require Bitwise

def start_link do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
Expand All @@ -22,7 +20,7 @@ defmodule XGPS.Parser do

defp unwrap_sentence(sentence) do
{body, checksum} = split(sentence)
calculated_checksum = calculate_checksum(body) |> XGPS.Tools.int_to_hex_string
calculated_checksum = XGPS.Tools.calculate_checksum(body) |> XGPS.Tools.int_to_hex_string
case calculated_checksum == checksum do
true -> {:ok, body}
false -> {:error,:checksum}
Expand All @@ -40,15 +38,6 @@ defmodule XGPS.Parser do
{main, checksum}
end

defp calculate_checksum text do
Enum.reduce(String.codepoints(text), 0, &xor/2)
end

defp xor(x, acc) do
<<val::utf8>> = x
Bitwise.bxor(acc, val)
end

defp get_type(["GPRMC"|content]), do: {:rmc, content}
defp get_type(["GPGGA"|content]), do: {:gga, content}
defp get_type(content), do: {:unknown, content}
Expand Down Expand Up @@ -141,37 +130,17 @@ defmodule XGPS.Parser do

defp parse_latitude("", ""), do: nil

defp parse_latitude(string, "N") do
value = parse_latitude_degrees(string)
value
end

defp parse_latitude(string, "S") do
value = parse_latitude_degrees(string)
value * (-1)
end

defp parse_latitude_degrees(string) do
defp parse_latitude(string, bearing) do
{deg, _} = String.slice(string,0,2) |> Float.parse
{min, _} = String.slice(string,2,100) |> Float.parse
deg + (min/60.0)
XGPS.Tools.lat_to_decimal_degrees(deg,min,bearing)
end

defp parse_longitude("", ""), do: nil

defp parse_longitude(string, "E") do
value = parse_longitude_degrees(string)
value
end

defp parse_longitude(string, "W") do
value = parse_longitude_degrees(string)
value * (-1)
end

defp parse_longitude_degrees(string) do
defp parse_longitude(string, bearing) do
{deg, _} = String.slice(string,0,3) |> Float.parse
{min, _} = String.slice(string,3,100) |> Float.parse
deg + (min/60.0)
XGPS.Tools.lon_to_decimal_degrees(deg,min,bearing)
end
end
17 changes: 11 additions & 6 deletions lib/port/reader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ defmodule XGPS.Port.Reader do
{:ok, state}
end

def init({:simulate}) do
gps_data = %XGPS.GpsData{has_fix: false}
state = %State{gps_data: gps_data, pid: :simulate, port_name: :simulate, data_buffer: ""}
{:ok, state}
end

def init({port_name}) do
{:ok, uart_pid} = Nerves.UART.start_link
:ok = Nerves.UART.open(uart_pid, port_name, speed: 9600, active: true)
Expand All @@ -55,11 +61,11 @@ defmodule XGPS.Port.Reader do
Nerves.UART.write(uart_pid, cmd6)
end

def handle_info({:nerves_uart, _port_name, "\n"}, %State{data_buffer: ""} = state) do
def handle_info({:nerves_uart, port_name, "\n"}, %State{port_name: port_name, data_buffer: ""} = state) do
{:noreply, state}
end

def handle_info({:nerves_uart, _port_name, "\n"}, state) do
def handle_info({:nerves_uart, port_name, "\n"}, %State{port_name: port_name} = state) do
sentence = String.strip((state.data_buffer))
Logger.debug(fn -> "Received: " <> sentence end)
parsed_data = XGPS.Parser.parse_sentence(sentence)
Expand All @@ -68,7 +74,7 @@ defmodule XGPS.Port.Reader do
{:noreply, %{state | data_buffer: "", gps_data: new_gps_data}}
end

def handle_info({:nerves_uart, _port_name, data}, state) do
def handle_info({:nerves_uart, port_name, data}, %State{port_name: port_name} = state) do
data_buffer = state.data_buffer <> data
{:noreply, %{state | data_buffer: data_buffer}}
end
Expand Down Expand Up @@ -124,9 +130,8 @@ defmodule XGPS.Port.Reader do
{:updated, new_gps_data}
end

defp update_gps_data(_message, gps_data) do
{:not_updated, gps_data}
end
# Any other message types
defp update_gps_data(_message, gps_data), do: {:not_updated, gps_data}

defp knots_to_kmh(speed_in_knots) when is_float(speed_in_knots) do
speed_in_knots * 1.852
Expand Down
27 changes: 24 additions & 3 deletions lib/port/supervisor.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@
defmodule XGPS.Port.Supervisor do
use Supervisor

def start_link(args) do
Supervisor.start_link(__MODULE__, args)
end

def get_gps_data(supervisor_pid) do
[{_, reader_pid, _, _}] = Supervisor.which_children(supervisor_pid)
XGPS.Port.Reader.get_gps_data(reader_pid)
end

def get_port_name(supervisor_pid) do
[{_, reader_pid, _, _}] = Supervisor.which_children(supervisor_pid)
XGPS.Port.Reader.get_port_name(reader_pid)
XGPS.Port.Reader.get_port_name(reader_pid)
end

def start_link(args) do
Supervisor.start_link(__MODULE__, args)
def send_simulated_data(supervisor_pid, sentence) do
[{_, reader_pid, _, _}] = Supervisor.which_children(supervisor_pid)
send reader_pid, {:nerves_uart, :simulate, sentence}
send reader_pid, {:nerves_uart, :simulate, "\r"}
send reader_pid, {:nerves_uart, :simulate, "\n"}
end

def send_simulated_position(supervisor_pid, lat, lon, alt, date_time) do
{rmc, gga} = XGPS.Tools.generate_rmc_and_gga_for_simulation(lat, lon, alt, date_time)
send_simulated_data(supervisor_pid, rmc)
send_simulated_data(supervisor_pid, gga)
:ok
end

def send_simulated_no_fix(supervisor_pid, date_time) do
{rmc, gga} = XGPS.Tools.generate_rmc_and_gga_for_simulation_no_fix(date_time)
send_simulated_data(supervisor_pid, rmc)
send_simulated_data(supervisor_pid, gga)
:ok
end

def init(args) do
Expand Down
40 changes: 40 additions & 0 deletions lib/ports_supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,39 @@ defmodule XGPS.Ports_supervisor do
end
end

def send_simulated_position(lat, lon, alt) when is_float(lat) and is_float(lon) and is_float(alt) do
now = DateTime.utc_now()
send_simulated_position(lat, lon, alt, now)
end

def send_simulated_position(lat, lon, alt, date_time) when is_float(lat) and is_float(lon) and is_float(alt) do
simulators = get_running_simulators()
case length(simulators) do
0 -> {:error, :no_simulator_running}
_ ->
{sim_pid, :simulate} = Enum.at(simulators, 0)

XGPS.Port.Supervisor.send_simulated_position(sim_pid , lat, lon, alt, date_time)
:ok
end
end

def send_simulated_no_fix() do
now = DateTime.utc_now()
send_simulated_no_fix(now)
end

def send_simulated_no_fix(date_time) do
simulators = get_running_simulators()
case length(simulators) do
0 -> {:error, :no_simulator_running}
_ ->
{sim_pid, :simulate} = Enum.at(simulators, 0)
XGPS.Port.Supervisor.send_simulated_no_fix(sim_pid, date_time)
:ok
end
end

def start_link do
result = {:ok, pid} = Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
start_port_if_defined_in_config(pid)
Expand All @@ -43,4 +76,11 @@ defmodule XGPS.Ports_supervisor do
]
supervise(children, strategy: :simple_one_for_one)
end

defp get_running_simulators do
Supervisor.which_children(__MODULE__)
|> Enum.map(fn({_, pid, :supervisor, _}) -> pid end)
|> Enum.map(fn(pid) -> {pid, XGPS.Port.Supervisor.get_port_name(pid)} end)
|> Enum.filter(fn({_pid, port_name}) -> port_name == :simulate end)
end
end
116 changes: 116 additions & 0 deletions lib/tools.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,125 @@
defmodule XGPS.Tools do
require Bitwise

def calculate_checksum text do
Enum.reduce(String.codepoints(text), 0, &xor/2)
end

defp xor(x, acc) do
<<val::utf8>> = x
Bitwise.bxor(acc, val)
end

def hex_string_to_int(string) do
string |> Base.decode16! |> :binary.decode_unsigned
end

def int_to_hex_string(int) do
int |> :binary.encode_unsigned |> Base.encode16
end

def lat_to_decimal_degrees(degrees, minutes, "N"), do: degrees + (minutes/60.0)
def lat_to_decimal_degrees(degrees, minutes, "S"), do: (degrees + (minutes/60.0)) * (-1.0)
def lon_to_decimal_degrees(degrees, minutes, "E"), do: degrees + (minutes/60.0)
def lon_to_decimal_degrees(degrees, minutes, "W"), do: (degrees + (minutes/60.0)) * (-1.0)

def lat_from_decimal_degrees(decimal_degrees) when decimal_degrees >= 0.0 do
degrees = Float.floor(decimal_degrees) |> round
minutes = (decimal_degrees - degrees) * 60.0
bearing = "N"
{degrees, minutes, bearing}
end

def lat_from_decimal_degrees(decimal_degrees) when decimal_degrees < 0.0 do
degrees = Float.ceil(decimal_degrees) * (-1.0) |> round
minutes = (decimal_degrees + degrees) * -60.0
bearing = "S"
{degrees, minutes, bearing}
end

def lon_from_decimal_degrees(decimal_degrees) when decimal_degrees >= 0.0 do
degrees = Float.floor(decimal_degrees) |> round
minutes = (decimal_degrees - degrees) * 60.0
bearing = "E"
{degrees, minutes, bearing}
end

def lon_from_decimal_degrees(decimal_degrees) when decimal_degrees < 0.0 do
degrees = Float.ceil(decimal_degrees) * (-1.0) |> round
minutes = (decimal_degrees + degrees) * -60.0
bearing = "W"
{degrees, minutes, bearing}
end

def to_gps_date(date) do
year = "#{date.year}" |> String.slice(2,2)
month = "#{date.month}" |> String.pad_leading(2,"0")
day = "#{date.day}" |> String.pad_leading(2,"0")
day <> month <> year
end

def to_gps_time(time) do
hour = "#{time.hour}" |> String.pad_leading(2,"0")
minute = "#{time.minute}" |> String.pad_leading(2,"0")
second = "#{time.second}" |> String.pad_leading(2,"0")
{micro, _} = time.microsecond
ms = round(micro / 1000)
millis = "#{ms}" |> String.pad_leading(3, "0")
"#{hour}#{minute}#{second}." <> millis
end

def generate_rmc_and_gga_for_simulation(lat, lon, alt, date_time) do
{lat_deg, lat_min, lat_bear} = XGPS.Tools.lat_from_decimal_degrees(lat)
{lon_deg, lon_min, lon_bear} = XGPS.Tools.lon_from_decimal_degrees(lon)

latitude = lat_to_string(lat_deg, lat_min, lat_bear)
longitude = lon_to_string(lon_deg, lon_min, lon_bear)

date = XGPS.Tools.to_gps_date(date_time)
time = XGPS.Tools.to_gps_time(date_time)

rmc_body = "GPRMC,#{time},A,#{latitude},#{longitude},0.0,0.0,#{date},,,A"
rmc_checksum = XGPS.Tools.calculate_checksum(rmc_body) |> XGPS.Tools.int_to_hex_string
rmc = "$#{rmc_body}*#{rmc_checksum}"
gga_body = "GPGGA,#{time},#{latitude},#{longitude},1,05,0.0,#{alt},M,0.0,M,,"
gga_checksum = XGPS.Tools.calculate_checksum(gga_body) |> XGPS.Tools.int_to_hex_string
gga = "$#{gga_body}*#{gga_checksum}"
{rmc, gga}
end

def generate_rmc_and_gga_for_simulation_no_fix(date_time) do
date = XGPS.Tools.to_gps_date(date_time)
time = XGPS.Tools.to_gps_time(date_time)
rmc_body = "GPRMC,#{time},V,,,,,,,#{date},,,A"
rmc_checksum = XGPS.Tools.calculate_checksum(rmc_body) |> XGPS.Tools.int_to_hex_string
rmc = "$#{rmc_body}*#{rmc_checksum}"
gga_body = "GPGGA,#{time},,,,,0,0,,,M,,M,,"
gga_checksum = XGPS.Tools.calculate_checksum(gga_body) |> XGPS.Tools.int_to_hex_string
gga = "$#{gga_body}*#{gga_checksum}"
{rmc, gga}
end

defp lat_to_string(deg, min, bearing) when min >= 10.0 do
deg_string = "#{deg}" |> String.pad_leading(2, "0")
min_string = "#{Float.round(min,4)}" |> String.pad_trailing(7, "0")
deg_string <> min_string <> "," <> bearing
end

defp lat_to_string(deg, min, bearing) do
deg_string = "#{deg}" |> String.pad_leading(2, "0")
min_string = "0#{Float.round(min,4)}" |> String.pad_trailing(7, "0")
deg_string <> min_string <> "," <> bearing
end

defp lon_to_string(deg, min, bearing) when min > 10.0 do
deg_string = "#{deg}" |> String.pad_leading(3, "0")
min_string = "#{Float.round(min,4)}" |> String.pad_trailing(7, "0")
deg_string <> min_string <> "," <> bearing
end

defp lon_to_string(deg, min, bearing) do
deg_string = "#{deg}" |> String.pad_leading(3, "0")
min_string = "0#{Float.round(min,4)}" |> String.pad_trailing(7, "0")
deg_string <> min_string <> "," <> bearing
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule XGPS.Mixfile do
def project do
[app: :xgps,
name: XGPS,
version: "0.1.0",
version: "0.2.0",
elixir: "~> 1.3",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
Expand Down
Loading

0 comments on commit 51c5993

Please sign in to comment.