Skip to content

Instantly share code, notes, and snippets.

@stefanbraun-private
Created April 27, 2024 09:48
Show Gist options
  • Save stefanbraun-private/fdb10090f10a91e3b324152ec9515062 to your computer and use it in GitHub Desktop.
Save stefanbraun-private/fdb10090f10a91e3b324152ec9515062 to your computer and use it in GitHub Desktop.
Pure-Python payload decoder for "SenseCAP T1000"(TM) GPS tracker (work in progress)
#!/usr/bin/python3
# Pure-Python payload decoder for "SenseCAP T1000"(TM) GPS tracker
# BrS, 27.April 2024
from construct import *
import binascii
import math
empty_uplink_format = Struct(
"bytes" / GreedyBytes
)
position_status_format = Struct("value" / Int8sb)
event_status_format = FlagsEnum(BytesInteger(3, signed=False, swapped=False),
no_event=0,
start_moving_event=1,
end_moving_event=2,
motionless_event=4,
shock_event=8,
temperature_event=16,
light_event=32,
sos_event=64,
press_once_button=128)
battery_percent_format = Struct("value" / Int8ub)
class VersionAdapter(Adapter):
""" mapping of array of two objects into float (example: 1.2) """
def _decode(self, obj, context, path):
major = obj[0]
minor = obj[1]
return float(str(major) + "." + str(minor))
def _encode(self, obj, context, path):
major, minor = str(obj).split(".")
return list(map(int, [major, minor]))
version_format = VersionAdapter(Int8ub[2])
work_mode_format = Struct(
"value" / Enum(Int8ub,
heartbeat_interval=0,
periodic_interval=1,
event_interval=2)
)
position_strategy_format = Struct(
# FIXME: this is a Enum. What are the values? =>Try all possibilities and watch on original decoder
"value" / Int8ub
)
heartbeat_interval_min_format = Struct("value" / Int16ub)
periodic_interval_min_format = Struct("value" / Int16ub)
event_interval_min_format = Struct("value" / Int16ub)
sensor_enable_format = Struct("value" / Flag)
sos_mode_format = Struct("value" / Flag)
motion_setting_format = Struct(
"enabled" / Flag,
"threshold" / Int16ub,
"interval_min" / Int16ub
)
static_setting_format = Struct(
"enabled" / Flag,
"timeout_min" / Int16ub
)
shock_setting_format = Struct(
"enabled" / Flag,
"threshold" / Int16ub
)
temperature_setting_format = Struct(
"enabled" / Flag,
"interval_min" / Int16ub,
"sample_interval_min" / Int16ub,
"threshold_max" / ExprAdapter(Int16ub,
lambda obj, ctx: obj / 10,
lambda obj, ctx: math.trunc(obj * 10)),
"threshold_min" / ExprAdapter(Int16ub,
lambda obj, ctx: obj / 10,
lambda obj, ctx: math.trunc(obj * 10)),
"warning" / Byte # FIXME: perhaps this is a Flag() ?
)
light_setting_format = Struct(
"enabled" / Flag,
"interval_min" / Int16ub,
"sample_interval_min" / Int16ub,
"threshold_max" / ExprAdapter(Int16ub,
lambda obj, ctx: obj / 10,
lambda obj, ctx: math.trunc(obj * 10)),
"threshold_min" / ExprAdapter(Int16ub,
lambda obj, ctx: obj / 10,
lambda obj, ctx: math.trunc(obj * 10)),
"warning" / Byte # FIXME: perhaps this is a Flag() ?
)
motion_id_format = Struct("value" / Int8ub)
utc_timestamp_format = Timestamp(Int32ub,
unit=1, # 1 second resolution
epoch=1970) # Unix epoch
longitude_format = ExprAdapter(Int32sb,
lambda obj, ctx: obj / 1_000_000,
lambda obj, ctx: math.trunc(obj * 1_000_000))
latitude_format = ExprAdapter(Int32sb,
lambda obj, ctx: obj / 1_000_000,
lambda obj, ctx: math.trunc(obj * 1_000_000))
air_temperature_format = ExprAdapter(Int16sb,
lambda obj, ctx: obj / 10,
lambda obj, ctx: math.trunc(obj * 10))
light_format = Struct("value" / Int16ub)
class MacAddressAdapter(Adapter):
def _decode(self, obj, context, path):
hex_strings = map(hex, obj)
return ":".join([my_hex[2:].zfill(2).upper() for my_hex in hex_strings])
def _encode(self, obj, context, path):
hex_strings = ["0x" + my_hex.upper() for my_hex in obj.split(":")]
return list(map(lambda x: int(x, base=0), hex_strings))
mac_rssi_format = Struct(
"mac" / MacAddressAdapter(Int8ub[6]),
"rssi" / Int8sb
)
shard_flag_format = Bitwise(Struct(
"count" / BitsInteger(4, signed=False, swapped=False),
"index" / BitsInteger(4, signed=False, swapped=False)
))
group_id_format = Struct(
"group_id" / Int32ub
)
frame_01_format = Struct(
"frame_type" / Const(b"\x01"),
"battery_percent" / battery_percent_format,
"firmware_version" / version_format,
"hardware_version" / version_format,
"work_mode" / work_mode_format,
"position_strategy" / position_strategy_format,
"heartbeat_interval_min" / heartbeat_interval_min_format,
"periodic_interval_min" / periodic_interval_min_format,
"event_interval_min" / event_interval_min_format,
"sensor_enable" / sensor_enable_format,
"sos_mode" / sos_mode_format,
"motion_setting" / motion_setting_format,
"static_setting" / static_setting_format,
"shock_setting" / shock_setting_format,
"temp_setting" / temperature_setting_format,
"light_setting" / light_setting_format
)
frame_02_format = Struct(
"frame_type" / Const(b"\x02"),
"battery_percent" / battery_percent_format,
"firmware_version" / version_format,
"hardware_version" / version_format,
"work_mode" / work_mode_format,
"position_strategy" / position_strategy_format,
"heartbeat_interval_min" / heartbeat_interval_min_format,
"periodic_interval_min" / periodic_interval_min_format,
"event_interval_min" / event_interval_min_format,
"sensor_enable" / sensor_enable_format,
"sos_mode" / sos_mode_format
)
frame_03_format = Struct(
"frame_type" / Const(b"\x03"),
"motion_setting" / motion_setting_format,
"static_setting" / static_setting_format,
"shock_setting" / shock_setting_format,
"temp_setting" / temperature_setting_format,
"light_setting" / light_setting_format
)
frame_04_format = Struct(
"frame_type" / Const(b"\x04"),
"work_mode" / work_mode_format,
"unknown_byte" / Byte, # FIXME: what is in this byte?!? Is this unused?
"heartbeat_interval_min" / heartbeat_interval_min_format,
"periodic_interval_min" / periodic_interval_min_format,
"event_interval_min" / event_interval_min_format,
"sos_mode" / sos_mode_format,
"uplink_interval_min" / Computed(this.heartbeat_interval_min if this.work_mode == 0 else this.periodic_interval_min if this.work_mode == 1 else this.event_interval_min)
)
frame_05_format = Struct(
"frame_type" / Const(b"\x05"),
"battery_percent" / battery_percent_format,
"work_mode" / work_mode_format,
"position_strategy" / position_strategy_format,
"sos_mode" / sos_mode_format
)
frame_06_format = Struct(
"frame_type" / Const(b"\x06"),
"event_status" / event_status_format,
"motion_id" / motion_id_format,
"utc_timestamp" / utc_timestamp_format,
"longitude" / longitude_format,
"latitude" / latitude_format,
"air_temperature" / air_temperature_format,
"light" / Int16ub,
"battery_percent" / battery_percent_format
)
frame_07_format = Struct(
"frame_type" / Const(b"\x07"),
"event_status" / event_status_format,
"motion_id" / motion_id_format,
"utc_timestamp" / utc_timestamp_format,
"wifi_scan" / Array(4, mac_rssi_format),
"air_temperature" / air_temperature_format,
"light" / Int16ub,
"battery_percent" / battery_percent_format
)
frame_08_format = Struct(
"frame_type" / Const(b"\x08"),
"event_status" / event_status_format,
"motion_id" / motion_id_format,
"utc_timestamp" / utc_timestamp_format,
"ble_scan" / Array(3, mac_rssi_format),
"air_temperature" / air_temperature_format,
"light" / Int16ub,
"battery_percent" / battery_percent_format
)
frame_09_format = Struct(
"frame_type" / Const(b"\x09"),
"event_status" / event_status_format,
"motion_id" / motion_id_format,
"utc_timestamp" / utc_timestamp_format,
"longitude" / longitude_format,
"latitude" / latitude_format,
"battery_percent" / battery_percent_format
)
frame_0a_format = Struct(
"frame_type" / Const(b"\x0a"),
"event_status" / event_status_format,
"motion_id" / motion_id_format,
"utc_timestamp" / utc_timestamp_format,
"wifi_scan" / Array(4, mac_rssi_format),
"battery_percent" / battery_percent_format
)
frame_0b_format = Struct(
"frame_type" / Const(b"\x0b"),
"event_status" / event_status_format,
"motion_id" / motion_id_format,
"utc_timestamp" / utc_timestamp_format,
"ble_scan" / Array(3, mac_rssi_format),
"battery_percent" / battery_percent_format
)
frame_0c_format = Struct(
"frame_type" / Const(b"\x0c"),
)
frame_0d_format = Struct(
"frame_type" / Const(b"\x0d"),
"error" / Enum(Int32ub,
failed_to_obtain_utc_timestamp=1,
almanac_too_old=2,
doppler_error=3)
)
frame_0e_format = Struct(
"frame_type" / Const(b"\x0e"),
"_value_part0" / Bytes(3),
"_remaining_size" / Int8ub,
"_value_part1" / Bytes(this._remaining_size),
# FIXME: how to combine "_value_partx" and extract the following information?
# perhaps with this method? https://stackoverflow.com/a/15539943
# FIXME: does "gnss_ng" have fixed length? Then no need to care about framesize...
#"shard_flag" / shard_flag_format,
#"group_id" / group_id_format,
#"gnss_ng" / xxx # unknown format... possibly latitude+longitude
)
frame_0f_format = Struct(
"frame_type" / Const(b"\x0f"),
"event_status" / event_status_format,
"motion_id" / motion_id_format,
"utc_timestamp" / utc_timestamp_format,
"air_temperature" / air_temperature_format,
"light" / light_format,
"battery_percent" / battery_percent_format,
"shard_flag" / shard_flag_format,
"group_id" / group_id_format
)
frame_10_format = Struct(
"frame_type" / Const(b"\x10"),
"event_status" / event_status_format,
"motion_id" / motion_id_format,
"utc_timestamp" / utc_timestamp_format,
"battery_percent" / battery_percent_format,
"shard_flag" / shard_flag_format,
"group_id" / group_id_format
)
frame_11_format = Struct(
"frame_type" / Const(b"\x11"),
"position_status" / position_status_format,
"event_status" / event_status_format,
"utc_timestamp" / utc_timestamp_format,
"air_temperature" / air_temperature_format,
"light" / light_format,
"battery_percent" / battery_percent_format
)
frames_format = Select(
"frame_01" / frame_01_format,
"frame_02" / frame_02_format,
"frame_03" / frame_03_format,
"frame_04" / frame_04_format,
"frame_05" / frame_05_format,
"frame_06" / frame_06_format,
"frame_07" / frame_07_format,
"frame_08" / frame_08_format,
"frame_09" / frame_09_format,
"frame_0a" / frame_0a_format,
"frame_0b" / frame_0b_format,
"frame_0c" / frame_0c_format,
"frame_0d" / frame_0d_format,
"frame_0e" / frame_0e_format,
"frame_0f" / frame_0f_format,
"frame_10" / frame_10_format,
"frame_11" / frame_11_format
)
normal_uplink_format = Struct(
"frames" / GreedyRange(frames_format)
)
codec_format = Switch(this.fPort, {192: empty_uplink_format,
199: empty_uplink_format,
5: normal_uplink_format},
default=GreedyBytes)
if __name__ == '__main__':
# test
for payload_hex in ["0224020501060105003c0001003c0100",
"0d00000001",
"0700000000662779e8ecf451f017f4ab6c996118df94a3a0b5490a6a89a5ec8eb5aa19b7a500f0002925"]:
payload_bytes = binascii.unhexlify(payload_hex)
print("payload_bytes={}".format(payload_bytes))
decoded = codec_format.parse(payload_bytes, fPort=5)
print("decoded={}".format(decoded))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment