scannit_core/
travelcard.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
use crate::conversion::*;
use crate::en1545date::{from_en1545_date, from_en1545_date_and_time};
use crate::eticket::*;
use crate::history::*;
use crate::models::*;
use chrono::prelude::*;

#[derive(Debug)]
pub struct TravelCard {
    // Application Info
    pub application_version: u8,
    pub application_key_version: u8,
    pub application_instance_id: String,
    pub platform_type: u8,
    pub is_mac_protected: bool,

    // Control info
    pub application_issuing_date: DateTime<Utc>,
    pub application_status: bool,
    pub application_unblocking_number: u8,
    pub application_transaction_counter: u32,
    pub action_list_counter: u32,

    // Period pass
    pub period_pass: PeriodPass,

    // Last load info
    pub stored_value_cents: u32,
    pub last_load_datetime: DateTime<Utc>,
    pub last_load_value: u32,
    pub last_load_organization_id: u16,
    pub last_load_device_num: u16,

    // E-Ticket
    pub e_ticket: ETicket,

    // History
    pub history: Vec<History>,
}

#[derive(Debug)]
pub struct PeriodPass {
    pub product_code_1: ProductCode,
    pub validity_area_1: ValidityArea,
    pub period_start_date_1: Date<Utc>,
    pub period_end_date_1: Date<Utc>,

    // This _seems_ to be the last-known season pass before the switchover to the new card format.
    // Probably part of the migration path when they were doing the changeover.
    pub product_code_2: ProductCode,
    pub validity_area_2: ValidityArea,
    pub period_start_date_2: Date<Utc>,
    pub period_end_date_2: Date<Utc>,

    // Most recent card load:
    pub loaded_period_product: ProductCode,
    pub loaded_period_datetime: DateTime<Utc>,
    pub loaded_period_length: u16,
    pub loaded_period_price: u32, // in cents
    pub loading_organization: u16,
    pub loading_device_number: u16,

    // Last use/boarding:
    pub last_board_datetime: DateTime<Utc>,
    pub last_board_vehicle_number: u16,
    pub last_board_location: BoardingLocation,
    pub last_board_direction: BoardingDirection,
    pub last_board_area: BoardingArea,
}

pub fn create_travel_card(
    app_info: &[u8],
    control_info: &[u8],
    period_pass: &[u8],
    stored_value: &[u8],
    e_ticket: &[u8],
    history: &[u8],
) -> TravelCard {
    println!(
        "Lengths: app_info: {:?}, period_pass: {:?}, history: {:?}",
        app_info.len(),
        period_pass.len(),
        history.len()
    );

    let (app_version, app_key_version, app_instance_id, platform, is_protected) =
        read_application_info(app_info);
    let (issue_date, app_status, unblock_number, transaction_counter, action_counter) =
        read_control_info(control_info);
    let period_pass = read_period_pass(period_pass);
    let stored_value = read_stored_value(stored_value);
    let e_ticket = create_e_ticket(e_ticket);
    let history = create_history_entries(history);

    TravelCard {
        application_version: app_version,
        application_key_version: app_key_version,
        application_instance_id: app_instance_id,
        platform_type: platform,
        is_mac_protected: is_protected,

        application_issuing_date: issue_date,
        application_status: app_status,
        application_unblocking_number: unblock_number,
        application_transaction_counter: transaction_counter,
        action_list_counter: action_counter,

        period_pass,

        stored_value_cents: stored_value.cents,
        last_load_datetime: stored_value.last_load_datetime,
        last_load_value: stored_value.last_load_value,
        last_load_organization_id: stored_value.last_load_organization_id,
        last_load_device_num: stored_value.last_load_device_num,

        e_ticket,
        history,
    }
}

// Notes about travel card data: All data is presented as a pile of bytes,
// and all bytes are expressed in Big Endian format.

fn read_application_info(app_info: &[u8]) -> (u8, u8, String, u8, bool) {
    (
        get_bits_as_u8(app_info, 0, 4),       // Application Version
        get_bits_as_u8(app_info, 4, 4), // Application Key Version (though the spec sheet marks it as "reserved")
        as_hex_string(&app_info[1..10]), // Application Instance ID (aka the card's unique ID number)
        get_bits_as_u8(app_info, 80, 3), // Platform Type, 0 = NXP DESFire 4kB.
        get_bits_as_u8(app_info, 83, 1) != 0, // SecurityLevel, which is a 1-bit field. 0 = open, 1 = MAC protected.
    )
}

fn read_control_info(control_info: &[u8]) -> (DateTime<Utc>, bool, u8, u32, u32) {
    let issuing_date = get_bits_as_u16(control_info, 0, 14);
    (
        from_en1545_date(issuing_date),
        get_bits_as_u8(control_info, 14, 1) != 0, // 1-bit app status (no idea what status *means*, but...)
        // Skip a single reserved bit here
        get_bits_as_u8(control_info, 16, 8), // 8-bit 'unblocking number' (ditto, no idea)
        get_bits_as_u32(control_info, 24, 24), // Application transaction counter, 24-bits long
        get_bits_as_u32(control_info, 48, 32), // Action List Counter, 32-bits long
    )
}

fn read_period_pass(period_pass: &[u8]) -> PeriodPass {
    let product_code_type_1 = get_bits_as_u8(period_pass, 0, 1);
    let product_code_1 = get_bits_as_u16(period_pass, 1, 14);
    let validity_area_type_1 = get_bits_as_u8(period_pass, 15, 2);
    let validity_area_1 = get_bits_as_u8(period_pass, 17, 6);
    let start_date_1 = get_bits_as_u16(period_pass, 23, 14);
    let end_date_1 = get_bits_as_u16(period_pass, 37, 14);
    let product_code_type_2 = get_bits_as_u8(period_pass, 56, 1);
    let product_code_2 = get_bits_as_u16(period_pass, 57, 14);
    let validity_area_type_2 = get_bits_as_u8(period_pass, 71, 2);
    let validity_area_2 = get_bits_as_u8(period_pass, 73, 6);
    let start_date_2 = get_bits_as_u16(period_pass, 79, 14);
    let end_date_2 = get_bits_as_u16(period_pass, 93, 14);

    let loaded_period_product_type = get_bits_as_u8(period_pass, 112, 1);
    let loaded_period_product = get_bits_as_u16(period_pass, 113, 14);
    let loaded_period_date = get_bits_as_u16(period_pass, 127, 14);
    let loaded_period_time = get_bits_as_u16(period_pass, 141, 11);
    let loaded_period_length = get_bits_as_u16(period_pass, 152, 9);
    let loaded_period_price = get_bits_as_u32(period_pass, 161, 20);
    let loading_organization = get_bits_as_u16(period_pass, 181, 14);
    let loading_device_number = get_bits_as_u16(period_pass, 195, 13);

    let last_board_date = get_bits_as_u16(period_pass, 208, 14);
    let last_board_time = get_bits_as_u16(period_pass, 222, 11);
    let last_board_vehicle_number = get_bits_as_u16(period_pass, 233, 14);
    let last_board_location_num_type = get_bits_as_u8(period_pass, 247, 2);
    let last_board_location_num = get_bits_as_u16(period_pass, 249, 14);
    let last_board_direction = get_bits_as_u8(period_pass, 263, 1);
    let last_board_area_type = get_bits_as_u8(period_pass, 264, 2);
    let last_board_area = get_bits_as_u8(period_pass, 266, 6);
    PeriodPass {
        product_code_1: ProductCode::new(product_code_type_1, product_code_1),
        validity_area_1: ValidityArea::new(validity_area_type_1, validity_area_1),
        period_start_date_1: from_en1545_date(start_date_1).date(),
        period_end_date_1: from_en1545_date(end_date_1).date(),

        product_code_2: ProductCode::new(product_code_type_2, product_code_2),
        validity_area_2: ValidityArea::new(validity_area_type_2, validity_area_2),
        period_start_date_2: from_en1545_date(start_date_2).date(),
        period_end_date_2: from_en1545_date(end_date_2).date(),

        loaded_period_product: ProductCode::new(loaded_period_product_type, loaded_period_product),
        loaded_period_datetime: from_en1545_date_and_time(loaded_period_date, loaded_period_time),
        loaded_period_length,
        loaded_period_price,
        loading_organization,
        loading_device_number,

        last_board_datetime: from_en1545_date_and_time(last_board_date, last_board_time),
        last_board_vehicle_number,
        last_board_location: BoardingLocation::new(
            last_board_location_num_type,
            last_board_location_num,
        ),
        last_board_direction: BoardingDirection::from(last_board_direction),
        last_board_area: BoardingArea::new(last_board_area_type, last_board_area),
    }
}

fn read_stored_value(stored_value: &[u8]) -> StoredValue {
    let last_load_date = get_bits_as_u16(stored_value, 20, 14);
    let last_load_time = get_bits_as_u16(stored_value, 34, 11);

    StoredValue {
        cents: get_bits_as_u32(stored_value, 0, 20),
        last_load_datetime: from_en1545_date_and_time(last_load_date, last_load_time),
        last_load_value: get_bits_as_u32(stored_value, 45, 20),
        last_load_organization_id: get_bits_as_u16(stored_value, 65, 14),
        last_load_device_num: get_bits_as_u16(stored_value, 79, 14),
    }
}

struct StoredValue {
    cents: u32,
    last_load_datetime: DateTime<Utc>,
    last_load_value: u32,
    last_load_organization_id: u16,
    last_load_device_num: u16,
}