Packages
teltonika_codec
0.1.0
Teltonika GPS tracker protocol parser for Elixir. Decodes Codec 8, Codec 8 Extended, and Codec 16 binary protocols used by Teltonika devices (TAT141, FMB920, FMC130, etc.).
Current section
Files
Jump to
Current section
Files
lib/teltonika_codec.ex
defmodule TeltonikaCodec do
@moduledoc """
Teltonika GPS tracker protocol parser for Elixir.
Parses the binary protocols used by Teltonika GPS tracking devices
(TAT141, FMB920, FMC130, TMT250, etc.) over TCP connections. Supports
Codec 8, Codec 8 Extended, and Codec 16.
## Quick Start
# Parse an IMEI handshake
{:ok, "352093085698206"} = TeltonikaCodec.parse_imei(data)
# Parse a data packet (auto-detects codec)
{:ok, records} = TeltonikaCodec.parse_packet(data)
## Protocol Overview
Teltonika devices communicate over TCP using a simple protocol:
1. Device connects and sends IMEI as `<<length::16, imei::binary>>`
2. Server responds with `0x01` (accept) or `0x00` (reject)
3. Device sends data packets containing AVL records
4. Server ACKs each packet with `<<record_count::32>>`
5. Repeat from step 3
## Supported Codecs
| Codec | ID | IO ID Size | IO Count Size | Variable IO | Generation Type |
|-------|------|------------|---------------|-------------|-----------------|
| 8 | 0x08 | 1 byte | 1 byte | No | No |
| 8 Ext | 0x8E | 2 bytes | 2 bytes | Yes (NX) | No |
| 16 | 0x10 | 2 bytes | 1 byte | No | Yes |
## Packet Structure
All codecs share the same outer frame:
<<0::32, data_length::32, data::binary, crc::32>>
The `data` field starts with a codec ID byte, then N AVL records, then
a repeat of the record count for validation.
"""
alias TeltonikaCodec.{Codec16, Codec8, Codec8Extended, CRC, IMEI}
@type priority :: :low | :high | :panic
@type avl_record :: %{
altitude: non_neg_integer(),
heading: non_neg_integer(),
io_elements: map(),
latitude: float(),
longitude: float(),
priority: priority(),
satellites: non_neg_integer(),
speed: non_neg_integer(),
timestamp: DateTime.t()
}
@type parse_result :: {:ok, [avl_record()]} | {:error, atom()}
@doc """
Parses an IMEI handshake packet.
The device sends `<<length::16, imei::binary-size(length)>>` on connect.
## Examples
iex> TeltonikaCodec.parse_imei(<<0, 15, "352093085698206">>)
{:ok, "352093085698206"}
iex> TeltonikaCodec.parse_imei(<<0, 5, "12345">>)
{:error, :invalid_imei_format}
"""
@spec parse_imei(binary()) :: {:ok, String.t()} | {:error, atom()}
defdelegate parse_imei(data), to: IMEI, as: :parse
@doc """
Parses a complete Codec 8/8E/16 data packet, auto-detecting the codec.
Returns `{:ok, records}` where records is a list of AVL record maps.
## Examples
iex> packet = Base.decode16!("000000000000002808010000016B40D9AD80010000000000000000000000000000000103021503010101425E100000010000F22A")
iex> {:ok, [record]} = TeltonikaCodec.parse_packet(packet)
iex> record.priority
:high
"""
@spec parse_packet(binary()) :: parse_result()
def parse_packet(<<0::32, data_length::32, data::binary-size(data_length), crc::32>>) do
if CRC.valid?(data, crc) do
dispatch_codec(data)
else
{:error, :crc_mismatch}
end
end
def parse_packet(<<0::32, _data_length::32, _rest::binary>>), do: {:error, :incomplete_packet}
def parse_packet(_), do: {:error, :malformed_packet}
@doc """
Checks if a complete packet is available in the buffer.
Returns `{:complete, packet_binary, remaining_buffer}` if a full packet
is available, `:incomplete` if more data is needed, or `{:error, reason}`.
Useful for accumulating TCP data until a full frame arrives.
## Examples
iex> packet = Base.decode16!("000000000000002808010000016B40D9AD80010000000000000000000000000000000103021503010101425E100000010000F22A")
iex> {:complete, ^packet, <<>>} = TeltonikaCodec.check_packet(packet)
iex> TeltonikaCodec.check_packet(<<0, 0, 0>>)
:incomplete
"""
@spec check_packet(binary()) :: {:complete, binary(), binary()} | :incomplete | {:error, atom()}
def check_packet(<<0::32, data_length::32, _rest::binary>> = buffer) do
total_size = 4 + 4 + data_length + 4
if byte_size(buffer) >= total_size do
<<packet::binary-size(total_size), remaining::binary>> = buffer
{:complete, packet, remaining}
else
:incomplete
end
end
def check_packet(buffer) when byte_size(buffer) < 8, do: :incomplete
def check_packet(_), do: {:error, :invalid_preamble}
@doc """
Builds an ACK response for the device.
The server must respond with the number of accepted data records as a
4-byte big-endian integer. **Failure to ACK causes the device to
retransmit, draining battery.**
## Examples
iex> TeltonikaCodec.ack(2)
<<0, 0, 0, 2>>
"""
@spec ack(non_neg_integer()) :: binary()
def ack(record_count), do: <<record_count::32>>
@doc """
Builds the IMEI accept response (`0x01`).
"""
@spec imei_accept() :: binary()
def imei_accept, do: <<0x01>>
@doc """
Builds the IMEI reject response (`0x00`).
"""
@spec imei_reject() :: binary()
def imei_reject, do: <<0x00>>
# --- Private ---
defp dispatch_codec(<<0x08, rest::binary>>), do: Codec8.parse_data(rest)
defp dispatch_codec(<<0x8E, rest::binary>>), do: Codec8Extended.parse_data(rest)
defp dispatch_codec(<<0x10, rest::binary>>), do: Codec16.parse_data(rest)
defp dispatch_codec(<<codec_id, _rest::binary>>) do
require Logger
Logger.warning("TeltonikaCodec: unsupported codec ID 0x#{Integer.to_string(codec_id, 16)}")
{:error, :unsupported_codec}
end
defp dispatch_codec(_), do: {:error, :malformed_data}
end