# SPDX-License-Identifier: GPL-2.0-only
# This file is part of Scapy
# See https://scapy.net/ for more information
# Copyright (C) 2019 Juciano Cardoso <cjuciano@gmail.com>
#               2019 Guillaume Valadon <guillaume.valadon@netatmo.com>

# scapy.contrib.description = Concox CRX1 unit tests
# scapy.contrib.status = loads

import binascii

from scapy.packet import Packet, bind_layers
from scapy.layers.inet import TCP, UDP
from scapy.fields import BitField, BitEnumField, X3BytesField, ShortField, \
    XShortField, FieldLenField, PacketLenField, XByteField, XByteEnumField, \
    ByteEnumField, StrFixedLenField, ConditionalField, FlagsField, ByteField, \
    IntField, XIntField, StrLenField, ScalingField

PROTOCOL_NUMBERS = {
    0x01: 'LOGIN MESSAGE',
    0x13: 'HEARTBEAT',
    0x12: 'LOCATION',
    0x16: 'ALARM',
    0x80: 'ONLINE COMMAND',
    0x15: 'ONLINE COMMAND REPLYED',
    0x94: 'INFORMATION TRANSMISSION',
}

SUBPROTOCOL_NUMBERS = {
    0x00: "EXTERNAL POWER VOLTAGE",
    0x04: "TERMINAL STATUS SYNCHRONIZATION",
    0x05: "DOOR STATUS",
}

VOLTAGE_LEVELS = {
    0x00: "No Power (Shutdown)",
    0x01: "Extremely Low Battery",
    0x02: "Very Low Battery",
    0x03: "Low Battery",
    0x04: "Medium",
    0x05: "High",
    0x06: "Very High",
}

GSM_SIGNAL_STRENGTH = {
    0x00: "No Signal",
    0x01: "Extremely Weak Signal",
    0x02: "Very Weak Signal",
    0x03: "Good Signal",
    0x04: "Strong Signal",
}

LANGUAGE = {
    0x01: "Chinese",
    0x02: "English",
}


class BCDStrFixedLenField(StrFixedLenField):
    def i2h(self, pkt, x):
        if isinstance(x, bytes):
            return binascii.b2a_hex(x)
        return binascii.a2b_hex(x)


class CRX1NewPacketContent(Packet):
    name = "CRX1 New Packet Content"
    fields_desc = [
        XByteEnumField('protocol_number', 0x12, PROTOCOL_NUMBERS),
        # Login
        ConditionalField(
            BCDStrFixedLenField('terminal_id', '00000000', length=8), lambda
            pkt: len(pkt.original) > 5 and pkt.protocol_number == 0x01),
        # GPS Location
        ConditionalField(
            ByteField('year', 0x00), lambda pkt: len(pkt.original) > 5 and pkt.
            protocol_number in (0x12, 0x16)),
        ConditionalField(
            ByteField('month', 0x01), lambda pkt: len(pkt.original) > 5 and pkt
            .protocol_number in (0x12, 0x16)),
        ConditionalField(
            ByteField('day', 0x01), lambda pkt: len(pkt.original) > 5 and pkt.
            protocol_number in (0x12, 0x16)),
        ConditionalField(
            ByteField('hour', 0x00), lambda pkt: len(pkt.original) > 5 and pkt.
            protocol_number in (0x12, 0x16)),
        ConditionalField(
            ByteField('minute', 0x00), lambda pkt: len(pkt.original) > 5 and
            pkt.protocol_number in (0x12, 0x16)),
        ConditionalField(
            ByteField('second', 0x00), lambda pkt: len(pkt.original) > 5 and
            pkt.protocol_number in (0x12, 0x16)),
        ConditionalField(
            BitField('gps_information_length', 0x00, 4), lambda pkt: len(
                pkt.original) > 5 and pkt.protocol_number in (0x12, 0x16)),
        ConditionalField(
            BitField('positioning_satellite_number', 0x00, 4), lambda pkt: len(
                pkt.original) > 5 and pkt.protocol_number in (0x12, 0x16)),
        ConditionalField(
            ScalingField('latitude', 0x00,
                         scaling=1.0 / 1800000, ndigits=6, fmt="!I"),
            lambda pkt: len(pkt.original) > 5 and \
            pkt.protocol_number in (0x12, 0x16)),
        ConditionalField(
            ScalingField('longitude', 0x00,
                         scaling=1.0 / 1800000, ndigits=6, fmt="!I"),
            lambda pkt: len(pkt.original) > 5 and \
            pkt.protocol_number in (0x12, 0x16)),
        ConditionalField(
            ByteField('speed', 0x00), lambda pkt: len(pkt.original) > 5 and pkt
            .protocol_number in (0x12, 0x16)),
        ConditionalField(
            BitField('course', 0x00, 10), lambda pkt: len(pkt.original) > 5 and
            pkt.protocol_number in (0x12, 0x16)),
        ConditionalField(
            BitEnumField('latitude_hemisphere', 0x00, 1, {
                0: "South",
                1: "North"
            }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (
                0x12, 0x16)),
        ConditionalField(
            BitEnumField('longitude_hemisphere', 0x00, 1, {
                0: "East",
                1: "West"
            }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (
                0x12, 0x16)),
        ConditionalField(
            BitEnumField('gps_been_positioning', 0x00, 1, {
                0: "No",
                1: "Yes"
            }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (
                0x12, 0x16)),
        ConditionalField(
            BitEnumField('gps_status', 0x00, 1, {
                0: "GPS real-time",
                1: "Differential positioning"
            }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (
                0x12, 0x16)),
        ConditionalField(
            BitField('course_status_reserved', 0x00, 2), lambda pkt: len(
                pkt.original) > 5 and pkt.protocol_number in (0x12, 0x16)),
        ConditionalField(
            ByteField('lbs_length', 0x00),
            lambda pkt: len(pkt.original) > 5 and \
            pkt.protocol_number in (0x16, )),
        ConditionalField(
            XShortField('mcc', 0x00), lambda pkt: len(pkt.original) > 5 and pkt
            .protocol_number in (0x12, 0x16)),
        ConditionalField(
            XByteField('mnc', 0x00), lambda pkt: len(pkt.original) > 5 and pkt.
            protocol_number in (0x12, 0x16)),
        ConditionalField(
            XShortField('lac', 0x00), lambda pkt: len(pkt.original) > 5 and pkt
            .protocol_number in (0x12, 0x16)),
        ConditionalField(
            X3BytesField('cell_id', 0x00),
            lambda pkt: len(pkt.original) > 5 and \
            pkt.protocol_number in (0x12, 0x16)),
        ConditionalField(
            IntField('mileage', 0x00), lambda pkt: len(pkt.original) > 5 and
            pkt.protocol_number in (0x12, ) and len(pkt.original) > 31),
        # Heartbeat
        ConditionalField(
            BitEnumField('defence', 0x00, 1, {
                0: "Deactivated",
                1: "Activated"
            }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (
                0x13, 0x16)),
        ConditionalField(
            BitEnumField('acc', 0x00, 1, {
                0: "Low",
                1: "High"
            }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (
                0x13, 0x16)),
        ConditionalField(
            BitEnumField('charge', 0x00, 1, {
                0: "Not Charge",
                1: "Charging"
            }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (
                0x13, 0x16)),
        ConditionalField(
            BitEnumField(
                'alarm', 0x00, 3, {
                    0: "Normal",
                    1: "Vibration",
                    2: "Power Cut",
                    3: "Low Battery",
                    4: "SOS"
                }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number
            in (0x13, 0x16)),
        ConditionalField(
            BitEnumField('gps_tracking', 0x00, 1, {
                0: "Not Charge",
                1: "Charging"
            }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (
                0x13, 0x16)),
        ConditionalField(
            BitEnumField('oil_and_eletricity', 0x00, 1, {
                0: "Connected",
                1: "Disconnected"
            }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (
                0x13, 0x16)),
        ConditionalField(
            ByteEnumField("voltage_level", 0x00, VOLTAGE_LEVELS), lambda pkt:
            len(pkt.original) > 5 and pkt.protocol_number in (0x13, 0x16)),
        ConditionalField(
            ByteEnumField("gsm_signal_strength", 0x00,
                          GSM_SIGNAL_STRENGTH), lambda pkt: len(pkt.original) >
            5 and pkt.protocol_number in (0x13, 0x16)),
        # Online Command
        ConditionalField(
            FieldLenField('command_length',
                          None,
                          fmt='B',
                          length_of="command_content"), lambda pkt:
            len(pkt.original) > 5 and pkt.protocol_number in (0x80, 0x15)),
        ConditionalField(
            XIntField('server_flag_bit', 0x00), lambda pkt: len(pkt.original) >
            5 and pkt.protocol_number in (0x80, 0x15)),
        ConditionalField(
            StrLenField(
                "command_content",
                "",
                length_from=lambda pkt: pkt.command_length - 4), lambda pkt:
            len(pkt.original) > 5 and pkt.protocol_number in (0x80, 0x15)),
        # Common
        ConditionalField(
            ByteEnumField(
                "alarm_extended", 0x00, {
                    0x00: "Normal",
                    0x01: "SOS",
                    0x02: "Power cut",
                    0x03: "Vibration",
                    0x04: "Enter fence",
                    0x05: "Exit fence",
                    0x06: "Over speed",
                    0x09: "Displacement",
                    0x0a: "Enter GPS dead zone",
                    0x0b: "Exit GPS dead zone",
                    0x0c: "Power on",
                    0x0d: "GPS First fix notice",
                    0x0e: "Low battery",
                    0x0f: "Low battery protection",
                    0x10: "SIM Change",
                    0x11: "Power off",
                    0x12: "Airplane mode",
                    0x13: "Disassemble",
                    0x14: "Door",
                    0xfe: "ACC On",
                    0xff: "ACC Off",
                }), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number
            in (0x13, 0x15, 0x16)),
        ConditionalField(
            ByteEnumField("language", 0x00,
                          LANGUAGE), lambda pkt: len(pkt.original) > 5 and pkt.
            protocol_number in (0x13, 0x15, 0x16)),
        # Information transmission
        ConditionalField(
            ByteEnumField("subprotocol_number", 0x00,
                          SUBPROTOCOL_NUMBERS), lambda pkt: len(pkt.original) >
            5 and pkt.protocol_number in (0x94, )),
        ConditionalField(
            ShortField('external_battery',
                       0x00), lambda pkt: len(pkt.original) > 5 and pkt.
            protocol_number in (0x94, ) and pkt.subprotocol_number == 0x00),
        ConditionalField(
            FlagsField('external_io_detection', 0x00, 8, [
                'door_status',
                'trigger_status',
                'io_status',
            ]), lambda pkt: len(pkt.original) > 5 and pkt.protocol_number in (
                0x94, ) and pkt.subprotocol_number == 0x05),
        # Default
        XShortField('information_serial_number', None),
        XShortField('crc', None),
    ]


class CRX1New(Packet):
    name = "CRX1 New"
    fields_desc = [
        XShortField('start_bit', 0x7878),
        ConditionalField(ByteField(
            'default_packet_length',
            None,
        ), lambda pkt: pkt.start_bit == 0x7878),
        ConditionalField(ShortField(
            'extended_packet_length',
            None,
        ), lambda pkt: pkt.start_bit == 0x7979),
        ConditionalField(
            PacketLenField('default_packet_content',
                           None,
                           CRX1NewPacketContent,
                           length_from=lambda pkt: pkt.default_packet_length),
            lambda pkt: pkt.start_bit == 0x7878),
        ConditionalField(
            PacketLenField('extended_packet_content',
                           None,
                           CRX1NewPacketContent,
                           length_from=lambda pkt: pkt.extended_packet_length),
            lambda pkt: pkt.start_bit == 0x7979),
        XShortField('end_bit', 0x0d0a),
    ]


bind_layers(TCP, CRX1New, sport=8821, dport=8821)
bind_layers(UDP, CRX1New, sport=8821, dport=8821)
