/*
* Copyright (c) 2019 Clementine Computing LLC.
*
* This file is part of PopuFare.
*
* PopuFare is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PopuFare is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with PopuFare. If not, see .
*
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include "tinyscheme1.39/scheme-private.h"
#include "tinyscheme1.39/scheme.h"
#include "../common/common_defs.h"
#include "../common/gpsmath.h"
#include "../commhub/commhub.h"
#include "../commhub/client_utils.h"
#include "passdb_slim.h"
#include "rules.h"
#include "rfid_decoder.h"
#define BUFFERSIZE (1024)
//Communication hub file descriptor...
extern int commhub_fd;
logical_card_id_t apb_cache[APB_CACHE_SIZE] = {0}; //Array to hold the anti-passback cache
time_t apb_time[APB_CACHE_SIZE] = {0}; //Array to hold the accept time of each row of the APB cache
int apb_idx = 0; //Index of the next record to fill in the anti-passback cache
double apb_lat = 0; //Last accept latitude for the anti-passback cache
double apb_lon = 0; //Last accept longitude " " " "
int apb_route = 0; //Last accept route
int apb_trip = 0; // trip
int apb_stop = 0; // stop
//This function clears the entire anti-passback cache
int apb_clear()
{
apb_idx = 0;
return 0;
}
//This function updates the location data for the anti-passback cache
static void update_apb_location()
{
apb_route = stop_stat.route;
apb_trip = stop_stat.trip;
apb_stop = stop_stat.stop;
apb_lat = effective_lat();
apb_lon = effective_lon();
}
// This function examines the state of the gps_stat and stop_stat structures and clears the anti-passback cache if the bus has
//either moved at least APB_DIST_EXPIRE meters since the last accept, or if the route, trip, or stop have changed.
// If the cache is cleared as the result of this function call, the cache state is updated so that it reflects the location
//data as they stood when the flush was completed. If there is nothing in the cache, the location is simply updated.
int apb_flush_if_needed()
{
float distance;
// struct message_record msg;
if(apb_idx == 0) //If the cache is empty, we can skip a lot of this stuff
{
// format_trace_message(&msg,"APBFIN: apb_idx == 0"); //WOOF
// if(commhub_fd) send_message(commhub_fd, &msg);
update_apb_location();
return 0;
}
//See if our route/trip/stop tuple has changed (if so we can skip the expensive GPS math)
if( (apb_route != stop_stat.route) || (apb_trip != stop_stat.trip) || (apb_stop != stop_stat.stop) )
{
// format_trace_message(&msg,"APBFIN: r/t/s != old r/t/s"); //WOOF
// if(commhub_fd) send_message(commhub_fd, &msg);
update_apb_location();
apb_clear();
return 1;
}
else //if not, see if the bus has at least moved
{
distance = GPS_Dist(apb_lat, apb_lon, effective_lat(), effective_lon());
if(distance >= APB_DIST_EXPIRE)
{
// format_trace_message(&msg,"APBFIN: distance > threshold"); //WOOF
// if(commhub_fd) send_message(commhub_fd, &msg);
update_apb_location();
apb_clear();
return 1;
}
}
// format_trace_message(&msg,"APBFIN: drop-through"); //WOOF
// if(commhub_fd) send_message(commhub_fd, &msg);
return 0;
}
//Add a rider to the anti-passback cache...
int apb_add(logical_card_id_t id)
{
//See if we need to auto-flush. If not, update the location marker of the APB cache to be "here"
if( !apb_flush_if_needed() )
{
update_apb_location();
}
//If we have space for more riders...
if(apb_idx < APB_CACHE_SIZE)
{
//set the time, logical_card_id, and increment the index counter
apb_time[apb_idx] = time(NULL);
apb_cache[apb_idx++] = id;
return 0;
}
else
{
return -1;
}
}
// Look up a rider in the anti-passback cache. If the rider is not present, this function returns zero.
//If the rider is present, this function returns the number of seconds since they last rode this bus.
int apb_lookup(logical_card_id_t id)
{
int i, j;
int age;
//Loop through all the entries in our anti-passback cache
for(i=0; i < apb_idx; i++)
{
if(apb_cache[i] == id) //if we've found one that matches this rider's ID number
{
age = time(NULL) - apb_time[i]; //figure out how old it is
if(age <= 0) //If the timestamp is very new
{
return 1; //pretend it's 1 second old (since 0 means "not found")
}
else if(age < APB_ROW_EXPIRE) //If the timestamp is not considered stale (older than APB_ROW_EXPIRE seconds...)
{
return age; //otherwise, return the age
}
else //If the timestamp _is_ stale, we want to purge just this row...
{
//We're deleting a row, so the index shrinks by one
apb_idx--;
//Bump any further entries back one (this is inefficient, but it almost never happens so we can eat it)
for(j = i; j < apb_idx; j++)
{
apb_time[j] = apb_time[j + 1];
apb_cache[j] = apb_cache[j + 1];
}
return 0; //Since the row we found expired, pretend it was never in there to being with
}
}
}
return 0; //return 0 if we didn't find the row...
}
// Takes a token as a first parameter, and a list of prohibited characters as a second.
//returns 1 if the first string contains any character from the second string.
//
// This is used to filter out credit card data from magstripe swipes. This function could later
//become much smarter and use regular expressions or some similar craziness, but for the moment this
//simple solution can handle every case we've seen to date.
static inline int magstripe_scrub_test(char *token, char *triggers)
{
char *p, *q;
if(! (token && triggers) ) //Don't even fuck with NULL pointers...
return 0;
p = token; //Set up a travelling pointer on to our token
while(*p) //While there's any more token
{
q = triggers; //Set up our travelling pointer to our trigger string
while(*q) //if there are any more triggers
{
if(*q == *p) //and we've hit one!
{
return 1; //return SCRUB.
}
else //otherwise,
{
q++; //try the next trigger
}
}
p++; //if this character of the token made it through unscathed, try the next
}
return 0; //by default, return 0 (DON'T scrub)
}
// This function does a "smart" magstripe lookup by looking up the token found
//on track1, then track2, then track3 individually (ignoring tracks 1 and 2 if they
//contain credit-card style field-delimiters), returning the index of the rider
//in the riders database attached to ctx, or -1 if not found.
// If the return is -1 (not found) final_token is filled with the whole list of
//tracks that were attempted (with credit-card looking tracks blanked out).
// If the return is >= 0 (record found) final_token is filled with the token that
//was used to generate the match.
//int smart_find_mag(passdb_context *ctx, char *token, char *final_token)
int smart_find_mag(passdb_slim_context *ctx, char *token, char *final_token)
{
char t1[BUFFERSIZE] = {0};
char t2[BUFFERSIZE] = {0};
char t3[BUFFERSIZE] = {0};
enum
{
GOT_TRACK_1 = 1,
GOT_TRACK_2 = 2,
GOT_TRACK_3 = 4
};
int track_map = 0;
char *trav;
int retval;
int i, idx;
trav = token;
idx = i = 0;
while(*trav)
{
switch(*trav)
{
case '%': //begin track 1
idx = 1;
track_map |= GOT_TRACK_1;
i = 0;
trav++;
break;
case ';': //begin track 2
// FIXME: Kludge. Our readers report a 2nd track2 for track2.
//the following if/else block handles that, so if there are two track2's,
//it silently makes the second one into a track3.
//
if( !(track_map & GOT_TRACK_2) ) //if we have no track2
{
idx = 2; //this track2 is the REAL track2
track_map |= GOT_TRACK_2;
}
else //otherwise, if we have already seen a track2
{
idx = 3; //pretend the 2nd track2 is really a track3
track_map |= GOT_TRACK_3;
}
i = 0;
trav++;
break;
case '+': //begin track 3
idx = 3;
track_map |= GOT_TRACK_3;
i = 0;
trav++;
break;
case '?': //end of track
switch(idx)
{
case 1:
t1[i++]='\0';
break;
case 2:
t2[i++]='\0';
break;
case 3:
t3[i++]='\0';
break;
default:
break;
}
idx = 0;
trav++;
break;
default:
switch(idx)
{
case 1:
t1[i++]=*trav;
break;
case 2:
t2[i++]=*trav;
break;
case 3:
t3[i++]=*trav;
break;
default:
break;
}
trav++;
break;
}
}
#ifdef SCRUB_MAGSTRIPE_TRACK1
//If there are any credit-card style field delimiters on track 1, null it out so we don't save any credit card #s
if( magstripe_scrub_test(t1, T1_SCRUB_LIST) )
{
t1[0] = '\0';
}
#endif
#ifdef SCRUB_MAGSTRIPE_TRACK2
//If there are any credit-card style field delimiters on track 2, null it out so we don't save any credit card #s
if( magstripe_scrub_test(t2, T2_SCRUB_LIST) )
{
t2[0] = '\0';
}
#endif
#ifdef SCRUB_MAGSTRIPE_TRACK3
//If there are any credit-card style field delimiters on track 3, null it out so we don't save any credit card #s
if( magstripe_scrub_test(t3, T3_SCRUB_LIST) )
{
t3[0] = '\0';
}
#endif
//------------------------- If we have a non-blank value for Track 1
if(t1[0] != '\0')
{
sprintf(final_token,"1:%s", t1); //Create a Track N hash key using the value on Track N
//"N:trackNdata"
retval = find_mag_in_hash(ctx, final_token); //look for a match
if(retval >= 0) //if we get it, use it.
{
return retval;
}
}
//------------------------- If we have a non-blank value for Track 1
if(t2[0] != '\0')
{
sprintf(final_token,"2:%s", t2); //Create a Track N hash key using the value on Track N
//"N:trackNdata"
retval = find_mag_in_hash(ctx, final_token); //look for a match
if(retval >= 0) //if we get it, use it.
{
return retval;
}
}
//------------------------- If we have a non-blank value for Track 3
if(t3[0] != '\0')
{
sprintf(final_token,"3:%s", t3); //Create a Track N hash key using the value on Track N
//"N:trackNdata"
retval = find_mag_in_hash(ctx, final_token); //look for a match
if(retval >= 0) //if we get it, use it.
{
return retval;
}
}
#ifdef ALLOW_WILDCARD_MAGSTRIPE_TRACK
//In each of these cases, we do a hash lookup for each non-blank track, but we create special hash keys
//which look up rows where the server has specified that ANY track will match if the data string matches
//These keys are in the form of "0:trackNdata", and after they match, they get rewritten so they appear
//in the credential data field of the billing record with a leading zero followed by the track that
//actually matched the wildcard. For example "02:track2data" would mean that the data on Track 2 was
//the first to match an all-tracks-wild card record.
//This is useful for cards which may have dodgey enough encoding that (and believe it or not, we've seen this)
//the reader reports the same card on successive swipes as having some given data on track 2 or track 3. This
//is an artifact of the fact that the readers in the current system are buggy in that they report track 3 as a second
//track 2, so on a card that counts on using track 3, but has a not-reliably-readable track 2, the actual value on
//track 3 will get reported as a track 2 value. This is immensely silly, but in the infinite wisdom of the firmware
//folks at UIC America, for compatibility with older systems, track 3 really *ought* to be a second track 2. *sigh*
if(t1[0] != '\0') //if a track is non-blank
{
sprintf(final_token,"0:%s", t1); //look it up with a track number of 0 (ANY)
retval = find_mag_in_hash(ctx, final_token);
if(retval >= 0) //if we found something...
{
sprintf(final_token,"01:%s", t1); //rewrite the credential to report wildcard match on THIS track
return retval; //and use the value
}
}
if(t2[0] != '\0')
{
sprintf(final_token,"0:%s", t2);
retval = find_mag_in_hash(ctx, final_token);
if(retval >= 0)
{
sprintf(final_token,"02:%s", t2);
return retval;
}
}
if(t3[0] != '\0')
{
sprintf(final_token,"0:%s", t3);
retval = find_mag_in_hash(ctx, final_token);
if(retval >= 0)
{
sprintf(final_token,"03:%s", t3);
return retval;
}
}
#endif
sprintf(final_token,"1:%s/2:%s/3:%s", t1, t2, t3);
return -1;
}
// This function does a "smart" RFID lookup by translating the raw RFID format (NN|XXXX...)
//where NN is the number of bits in hex, and XXXX... is the bitstring in hex (right justified
//to the nearest nibble boundary) and processing it based on the number of bits and a list of
//known formats until it either matches a known format or is determined not to match any of them.
// If a match is found, the return value will be >= 0 and indicate the index of the rider in
//the database that ctx is attached to. If no match is found, -1 is returned. In either case
//final_token is filled with the string representing the decoded decimal RFID value "nbits:site:id"
//or if the token was not in a known format, "nbits:rawval?".
//int smart_find_rf(passdb_context *ctx, char *token, char *final_token)
int smart_find_rf(passdb_slim_context *ctx, char *token, char *final_token)
{
unsigned long long site;
unsigned long long id;
unsigned int nbits;
int retval;
retval = decode_rfid_string(token, &site, &id, &nbits);
if(retval < 0)
{
sprintf(final_token,"%s", token);
return -1;
}
else
{
sprintf(final_token, "%d:%lld:%lld", nbits, site, id);
return find_rf_in_hash(ctx, final_token);
}
}
//-------------------------------------------------------------- TEMPORARY OVERLAY DATA -----------------------------------------------------
typedef struct rule_param_lookaside_struct
{
seq_t seq;
logical_card_id_t id;
char rule_param[PARAM_LEN];
struct rule_param_lookaside_struct *next;
} rule_param_lookaside;
static rule_param_lookaside *local_param = NULL;
#define FIND_LOOKASIDE_GUTS(p, q, id) {q = NULL; p = local_param; while(p) { if(p->id == (id)) break; q = p; p = p->next; }}
int set_lookaside(seq_t seq, logical_card_id_t id, char *param)
{
rule_param_lookaside *p, *q;
FIND_LOOKASIDE_GUTS(p, q, id);
if(!p)
{
p = (rule_param_lookaside *)malloc(sizeof(rule_param_lookaside));
if(!p)
{
fprintf(stderr, "Cannot allocate memory for lookaside buffer!\n");
fflush(stderr);
return -1;
}
p->next = NULL;
p->seq = seq;
p->id = id;
if(q)
{
q->next = p;
}
else
{
local_param = p;
}
}
strncpy(p->rule_param, param, PARAM_LEN - 1);
p->rule_param[PARAM_LEN - 1] = '\0';
return 0;
}
int check_lookaside(seq_t delete_if_older, logical_card_id_t id, char *param)
{
rule_param_lookaside *p, *q;
FIND_LOOKASIDE_GUTS(p, q, id);
if(p)
{
if(p->seq < delete_if_older)
{
if(q)
{
q->next = p->next;
}
else
{
local_param = p->next;
}
free(p);
return 0;
}
strncpy(param, p->rule_param, PARAM_LEN - 1);
param[PARAM_LEN - 1] = '\0';
return 1;
}
else
{
return 0;
}
}
//-------------------------------------------------------------------------------------------------------------------------------------------
//--------------------------------------BELOW HERE, WE HANDLE RULE PROCESSING LOGIC BY FEEDING THE SCHEME INTERPRETER RULE CALLS AND SEEING
//--------------------------------------WHICH CALLBACK (ACCEPT/REJECT) IT CALLS. "Here Be Dragons..."
//-------------------------------------------------------------------------------------------------------------------------------------------
/*
A C programmer's crash course in the main working data structure of the scheme programmer...
The base data structure is a pair of pointers. While they can be arranged in all sorts of ways (lists, trees, etc...) the list is the base form.
The first pointer is called the CAR pointer (Content Address Register) and the second the CDR (Content Data Register). This has no bearing on how
they're used, but that's what they're called. By convention in a list, you have your CAR pointer pointing to a list item (a garbage collected cell of
managed memory), and your CDR pointer pointing to the next pair in the list. In the case of trees and nested lists you'll have things like the second
example below where the CAR pointer points at a pair rather than a value, in which case we have a sub-list.
the operations that manipulate lists at their most basic are these three, cons, car, and cdr. cons constructs a list, so the following:
(cons 1 (cons 2 (cons 3 '()))) will return (1 2 3)
car and cdr get the pointers contained in the CAR and CDR of a pair, so (car '(1 2 3)) will return 1, wheras (cdr '(1 2 3)) will return (2 3).
(cdr '(1)) will return NIL.
'(1 2 3.1 4) works out to:
car cdr
___________ ___________ ___________ ___________
| | | | | | | | | | | |
| * | *--|--->| * | *--|--->| * | *--|--->| * | NIL |
|__|__|_____| |__|__|_____| |__|__|_____| |__|__|_____|
| | | |
v v v v
1 2 3.1 4
'(a b (c d) e) works out to:
car cdr
___________ ___________ ___________ ___________
| | | | | | | | | | | |
| * | *--|--->| * | *--|--->| * | *--|--->| * | NIL |
|__|__|_____| |__|__|_____| |__|__|_____| |__|__|_____|
| | | |
v v | v
|
'a' 'b' | 'e'
|
V
___________ ___________
| | | | | |
| * | *--|--->| * | NIL |
|__|__|_____| |__|__|_____|
| |
v v
'c' 'd'
The file scheme.h defines the interface we use below to bind C functions to Scheme symbols. Most of the work in the below functions is in decoding the
above data structures into something we can use in C. This is a cumbersome process in C, but not unmanagable. The arguments of any our our wrapper
functions are two items, first a pointer to the interpreter context in which the function has been called, and second a pointer to a list of arguments.
this pointer is in the form of the first list above, so the first CAR pointer points at the first argument, and the CDR points at the rest of the list.
*/
//Message structures to fill with messages resuling from rule processing
struct message_record bill_msg;
struct message_record user_msg;
struct message_record rider_msg;
//List of feedback flags which will be set during rule processing
static int scheme_error = 0;
static int accept_flag = 0;
static int add_to_passback_flag = 0;
//List of state variables which need to be bound by the environment before calling a rule
static char *rname = NULL;
static char *rparam = NULL;
static char *cred = NULL;
static logical_card_id_t logical_card_id = 0;
static seq_t seq_num;
pointer debug_print_nonl(scheme *sc, pointer args) //this is used to print scheme data structures out to stdout, it's recursive and hairy, but for
{ //debugging it's worth its weight in gold.
pointer car,trav;
if(args != sc->NIL) //if we have arguments to process
{
trav=args;
while(trav != sc->NIL) //walk the arguments list
{
if(trav != args)
printf(" "); //if we're not on our first argument, put a space between
car=pair_car(trav); //get the CAR pointer
if(is_pair(car)) //if it points to a pair, we're looking at a sublist
{
printf("("); //print the parens
debug_print_nonl(sc,car); //and recurse
printf(")"); //close 'em
}
else //if it's not a pair, find out what sort of a value it is
{
if(is_string(car)) //try string
{
printf("\"%s\"",string_value(car));
}
else if(is_symbol(car)) //then symbol
{
printf("'%s",symname(car));
}
else if(is_integer(car)) //int
{
printf("%ld",ivalue(car));
}
else if(is_real(car)) //or real
{
printf("%f",rvalue(car));
}
else
{
printf("?");
}
}
trav=pair_cdr(trav); //advance our travelling pointer
}
}
return sc->NIL;
}
pointer debug_print(scheme *sc, pointer args)
{
debug_print_nonl(sc,args);
printf("\n");
return sc->NIL;
}
pointer tell_rider(scheme *sc, pointer args)
{
format_piu_message(&rider_msg, 1, PIU_PRIORITY_FARE, PASSENGER_MESSAGE_DURATION, "%s", string_value(pair_car(args)));
if(commhub_fd >= 0)
{
send_message(commhub_fd, &rider_msg);
}
return sc->NIL;
}
pointer accept_fare_no_passback(scheme *sc, pointer args) //this function provides a scheme interface to accept a fare
{
pointer trav,car;
int idx = 0;
char reason[BILLING_REASON_LEN] = {0};
int cash_val = 0;
int user_msg_good = 0;
trav = args;
while(trav != sc->NIL) //while we have more data
{
car=pair_car(trav); //the car pointer should contain the parameter
trav=pair_cdr(trav); //the cdr pointer points at the rest of the list
switch(idx)
{
case 0: //First parameter is the accept reason
strncpy(reason, string_value(car), BILLING_REASON_LEN);
reason[BILLING_REASON_LEN - 1] = '\0';
break;
case 1: //Second parameter is the display reason
format_driver_message(&user_msg, LOGLEVEL_ACCEPT, "%s %s", ACCEPT_STR, string_value(car));
user_msg_good = 1;
break;
case 2: //Third parameter is the cash value if any...
cash_val = ivalue(car);
break;
default:
break;
}
idx++;
if(idx > 2)
break;
}
format_billing_message(&bill_msg, ACCEPT_STR, rname, rparam, reason, cred, logical_card_id, cash_val);
if(commhub_fd >= 0)
{
send_message(commhub_fd, &bill_msg);
if(user_msg_good)
{
send_message(commhub_fd, &user_msg);
}
accept_flag = 1;
add_to_passback_flag = 0;
}
else
{
//TODO: What the hell do we do now?
}
return sc->NIL;
}
pointer drop_vault(scheme *sc, pointer args)
{
prepare_message(&user_msg, MAILBOX_VAULT_DROP, "", 0);
if(commhub_fd >= 0)
{
send_message(commhub_fd, &user_msg);
}
return sc->NIL;
}
pointer accept_fare(scheme *sc, pointer args) //this function provides a scheme interface to accept a fare
{
pointer trav,car;
int idx = 0;
char reason[BILLING_REASON_LEN] = {0};
int cash_val = 0;
int user_msg_good = 0;
trav = args;
while(trav != sc->NIL) //while we have more data
{
car = pair_car(trav); //the car pointer should contain the parameter
trav = pair_cdr(trav); //the cdr pointer points at the rest of the list
switch(idx)
{
case 0: //First parameter is the accept reason
strncpy(reason, string_value(car), BILLING_REASON_LEN);
reason[BILLING_REASON_LEN - 1] = '\0';
break;
case 1: //Second parameter is the display reason
format_driver_message(&user_msg, LOGLEVEL_ACCEPT, "%s %s", ACCEPT_STR, string_value(car));
user_msg_good = 1;
break;
case 2: //Third parameter is the cash value if any...
cash_val = ivalue(car);
break;
default:
break;
}
idx++;
if(idx > 2)
break;
}
format_billing_message(&bill_msg, ACCEPT_STR, rname, rparam, reason, cred, logical_card_id, cash_val);
if(commhub_fd >= 0)
{
send_message(commhub_fd, &bill_msg);
if(user_msg_good)
{
send_message(commhub_fd, &user_msg);
}
accept_flag = 1;
add_to_passback_flag = 1;
}
else
{
//TODO: What the hell do we do now?
}
return sc->NIL;
}
pointer reject_fare(scheme *sc, pointer args) //this function provides a scheme interface to reject a fare
{
pointer trav,car;
int idx = 0;
char reason[BILLING_REASON_LEN] = {0};
int cash_val = 0;
int user_msg_good = 0;
trav = args;
while(trav != sc->NIL) //while we have more data
{
car=pair_car(trav); //the car pointer should contain the parameter
trav=pair_cdr(trav); //the cdr pointer points at the rest of the list
switch(idx)
{
case 0: //First parameter is the accept reason
strncpy(reason, string_value(car), BILLING_REASON_LEN);
reason[BILLING_REASON_LEN - 1] = '\0';
break;
case 1: //Second parameter is the display reason
format_driver_message(&user_msg, LOGLEVEL_REJECT, "%s %s", REJECT_STR, string_value(car));
user_msg_good = 1;
break;
default:
break;
}
idx++;
if(idx > 1)
break;
}
format_billing_message(&bill_msg, REJECT_STR, rname, rparam, reason, cred, logical_card_id, cash_val);
if(commhub_fd >= 0)
{
send_message(commhub_fd, &bill_msg);
if(user_msg_good)
{
send_message(commhub_fd, &user_msg);
}
accept_flag = 0;
add_to_passback_flag = 0;
}
else
{
//TODO: What the hell do we do now?
}
return sc->NIL;
}
pointer handle_rule_error(scheme *sc, pointer args) //this error handler allows any errors or exceptions like dereferencing NIL's or
{ //syntax errors in the scheme rules to be reported up to the driver (and also keep them from
char err[1024]; //causing all hell to break loose). Think of it as a catch block.
struct message_record logmsg;
pointer car = pair_car(args); //get the pointer to our first argument's value (the error message)
pointer cadr = pair_car(pair_cdr(args)); //get the pointer to our next argument (the optional identifier)
scheme_error=1; //set the error flag
if(is_symbol(cadr))
{
sprintf(err,"\"%s\" %s", string_value(car), symname(cadr)); //spit that argument out on the UI
}
else if(is_string(cadr))
{
sprintf(err,"\"%s\" \"%s\"", string_value(car), string_value(cadr)); //spit that argument out on the UI
}
else
{
sprintf(err,"\"%s\" ", string_value(car)); //spit that argument out on the UI
}
//Construct a diagnostic log message for this error
format_log_message(&logmsg, LOGLEVEL_ERROR, "Scheme Error: %s", err);
if(commhub_fd >= 0) //If we have a connection to the commhub
{
send_message(commhub_fd, &logmsg); //send this driver message to the log
}
return args; // Although it's not documented, if the error handler does not return its arguments the
//scheme interpreter throws a SEGV so don't mess with this return value unless you're very brave
//or have some profound insight into the inner workings of the scheme interpreter that you are
//pretty sure will never go obsolete with version upgrades...
}
pointer get_stopnum(scheme *sc, pointer args)
{
return mk_integer(sc,stop_stat.stop);
}
pointer get_routenum(scheme *sc, pointer args) //returns the human-friendly route 30, 92, etc...
{
return mk_integer(sc,(int)(stop_stat.route / 100));
}
pointer get_longroutenum(scheme *sc, pointer args) //returns the "internal" route number 3000 = 30 outbound, 3010 = 30 inbound, etc...
{
return mk_integer(sc,stop_stat.route);
}
pointer get_tripnum(scheme *sc, pointer args)
{
return mk_integer(sc,stop_stat.trip);
}
pointer get_time(scheme *sc, pointer args) //this function returns the time in the form (hour minute) example (13 5) 1:05pm
{
time_t t;
struct tm comp;
t=time(NULL);
localtime_r(&t,&comp);
return cons(sc,mk_integer(sc,comp.tm_hour),cons(sc,mk_integer(sc,comp.tm_min),sc->NIL));
}
pointer get_date(scheme *sc, pointer args) //this function returns the date in the form (year month day) example (2007 5 17) May 17th 2007
{
time_t t;
struct tm comp;
t=time(NULL);
localtime_r(&t,&comp);
return cons(sc,mk_integer(sc,comp.tm_year + 1900),cons(sc,mk_integer(sc,comp.tm_mon + 1),cons(sc,mk_integer(sc,comp.tm_mday),sc->NIL)));
}
pointer get_day_of_week(scheme *sc, pointer args) //this function returns a number 1-7 inclusive, 1=Sunday 7=Saturday
{
time_t t;
struct tm comp;
t=time(NULL);
localtime_r(&t,&comp);
return mk_integer(sc,comp.tm_wday + 1);
}
pointer get_gps(scheme *sc, pointer args) //this function returns the current GPS coordinates (latitude longitude) where S < 0 > N and W < 0 > E
{
if(gps_stat.gps_good)
{
return cons(sc,mk_real(sc,gps_stat.lat),cons(sc,mk_real(sc,gps_stat.lon),sc->NIL));
}
else
{
return cons(sc,mk_real(sc,stop_stat.lat),cons(sc,mk_real(sc,stop_stat.lon),sc->NIL));
}
}
pointer get_rulename(scheme *sc, pointer args) //returns the name of the current rule (useful for billing logs)
{
if(rname)
{
return mk_string(sc,rname);
}
else
{
return sc->NIL;
}
}
pointer get_ruleparam(scheme *sc, pointer args) //returns the parameter of the current rule
{
if(rparam)
{
return mk_string(sc,rparam);
}
else
{
return sc->NIL;
}
}
pointer scheme_set_lookaside(scheme *sc, pointer args)
{
char buffer[PARAM_LEN] ={0};
pointer arg;
if(args == sc->NIL)
{
return sc->NIL;
}
arg = pair_car(args);
if(arg != sc->NIL)
{
if(is_string(arg)) //try string
{
set_lookaside(seq_num, logical_card_id, string_value(arg));
return sc->NIL;
}
else if(is_integer(arg)) //int
{
sprintf(buffer, "%ld", ivalue(arg));
set_lookaside(seq_num, logical_card_id, buffer);
return sc->NIL;
}
else if(is_real(arg)) //or real
{
sprintf(buffer, "%f", rvalue(arg));
set_lookaside(seq_num, logical_card_id, buffer);
return sc->NIL;
}
}
return sc->NIL;
}
pointer scheme_get_lookaside(scheme *sc, pointer args)
{
char buffer[PARAM_LEN] = {0};
if(check_lookaside(seq_num, logical_card_id, buffer))
{
return mk_string(sc, buffer);
}
else
{
return sc->NIL;
}
}
pointer get_lookaside_param(scheme *sc, pointer args)
{
pointer foo;
foo = scheme_get_lookaside(sc, args);
if(foo != sc->NIL)
{
// printf("Using estimate!\n");
return foo;
}
else
{
// printf("Using real data!\n");
return get_ruleparam(sc, args);
}
}
pointer scheme_strtok(scheme *sc, pointer args)
{
pointer p, q, ret;
char *tmp = NULL;
char *delim = NULL;
char *token = NULL;
char chop_buffer[LINE_BUFFER_SIZE] = {0};
p = q = ret = sc->NIL;
if(!is_string(pair_car(args)))
{
return sc->NIL;
}
if(!is_string(pair_car(pair_cdr(args))))
{
return sc->NIL;
}
strncpy(chop_buffer, string_value(pair_car(args)), LINE_BUFFER_SIZE);
chop_buffer[LINE_BUFFER_SIZE - 1] = '\0';
delim = string_value(pair_car(pair_cdr(args)));
token = strtok_r(chop_buffer, delim, &tmp);
while(token)
{
if(q == sc->NIL)
{
q = ret = cons(sc, mk_string(sc, token), sc->NIL);
}
else
{
p = cons(sc, mk_string(sc, token), sc->NIL);
set_cdr(q, p);
q = p;
}
token = strtok_r(NULL, delim, &tmp);
}
return ret;
}
pointer gps_distance(scheme *sc, pointer args) //this function takes two sets of coordinates (as returned by get_gps) and returns a distance in meters
{
double la1,lo1,la2,lo2;
double dist;
pointer p1,p2;
if(!is_pair(args))
return sc->NIL;
p1=pair_car(args);
if(!is_pair(pair_cdr(args)))
return sc->NIL;
p2=pair_car(pair_cdr(args));
if(!is_real(pair_car(p1)))
return sc->NIL;
if(!is_real(pair_car(pair_cdr(p1))))
return sc->NIL;
if(!is_real(pair_car(p2)))
return sc->NIL;
if(!is_real(pair_car(pair_cdr(p2))))
return sc->NIL;
la1=rvalue(pair_car(p1));
lo1=rvalue(pair_car(pair_cdr(p1)));
la2=rvalue(pair_car(p2));
lo2=rvalue(pair_car(pair_cdr(p2)));
dist=GPS_Dist(la1,lo1,la2,lo2);
// printf("Distace: (%f %f) -> (%f %f) = %f m\n",la1,lo1,la2,lo2,dist);
return mk_real(sc,dist);
}
scheme scm={0};
int rules_loaded_flag = 0;
#define SCHEME_INTERNAL_ERROR(x) ((x)->retcode != 0)
int rules_loaded()
{
return rules_loaded_flag;
}
int unload_rules()
{
if(rules_loaded_flag)
{
scheme_deinit(&scm);
}
rules_loaded_flag = 0;
return 0;
}
int load_rules(char *filename)
{
FILE *f;
struct message_record logmsg;
scheme_init(&scm);
// This is of vital importance... Failure to do so will cause error messages that
//don't get caught by the *error-hook* exception handler due to a bug in tinyscheme
//to try and output to a zero-length string output port which will then cause a SIGSEGV
//and undignified exit-and-respawn of passdb. When this happens we loose the error message
//itself, so the rule error becomes a hell of a lot harder to debug.
scheme_set_output_port_file(&scm, stdout);
f=fopen(SCHEME_INIT_FILE,"rb"); //digest the standard library
if(!f)
{
//complain violently if it fails to exist
format_log_message(&logmsg, LOGLEVEL_ERROR, "File error loading %s library.", SCHEME_INIT_FILE);
if(commhub_fd >= 0) //If we have a connection to the commhub
{
send_message(commhub_fd, &logmsg); //send this driver message to the log
}
return -1;
}
scheme_error=0; //clear our error callback flag
scheme_load_file(&scm,f); //try and load it
fclose(f); //close the file
if( SCHEME_INTERNAL_ERROR(&scm) )
{
scheme_deinit(&scm);
//complain violently if it fails to parse
format_log_message(&logmsg, LOGLEVEL_ERROR, "Scheme error loading %s library.", SCHEME_INIT_FILE);
if(commhub_fd >= 0) //If we have a connection to the commhub
{
send_message(commhub_fd, &logmsg); //send this driver message to the log
}
return -1;
}
//set up our error handler so we can detect bogus or broken rule loads
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"debug_print"),mk_foreign_func(&scm, debug_print));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"handle_rule_error"),mk_foreign_func(&scm, handle_rule_error));
scheme_load_string(&scm,"(define *error-hook* handle_rule_error)");
//and initialize all of the symbols for our wrapper functions
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"stopnum"),mk_foreign_func(&scm, get_stopnum));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"routenum"),mk_foreign_func(&scm, get_routenum));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"longroutenum"),mk_foreign_func(&scm, get_longroutenum));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"tripnum"),mk_foreign_func(&scm, get_tripnum));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"gps"),mk_foreign_func(&scm, get_gps));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"rule"),mk_foreign_func(&scm, get_rulename));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"param"),mk_foreign_func(&scm, get_ruleparam));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"get_lookaside"),mk_foreign_func(&scm, scheme_get_lookaside));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"set_lookaside"),mk_foreign_func(&scm, scheme_set_lookaside));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"lookaside_param"),mk_foreign_func(&scm, get_lookaside_param));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"gettime"),mk_foreign_func(&scm, get_time));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"day_of_week"),mk_foreign_func(&scm, get_day_of_week));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"getdate"),mk_foreign_func(&scm, get_date));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"gps_distance"),mk_foreign_func(&scm, gps_distance));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"accept_fare"),mk_foreign_func(&scm, accept_fare));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"accept_fare_no_passback"),mk_foreign_func(&scm, accept_fare_no_passback));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"reject_fare"),mk_foreign_func(&scm, reject_fare));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"tell_rider"),mk_foreign_func(&scm, tell_rider));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"drop_vault"),mk_foreign_func(&scm, drop_vault));
scheme_define(&scm,scm.global_env,mk_symbol(&scm,"strtok"),mk_foreign_func(&scm, scheme_strtok));
f=fopen(filename,"rb"); //open the rules file
if(!f)
{
scheme_deinit(&scm);
//complain violently if it fails to exist
format_log_message(&logmsg, LOGLEVEL_ERROR, "File error loading rules from %s", filename);
if(commhub_fd >= 0) //If we have a connection to the commhub
{
send_message(commhub_fd, &logmsg); //send this driver message to the log
}
return -1;
}
scheme_error=0;
scheme_load_file(&scm,f);
if( scheme_error || SCHEME_INTERNAL_ERROR(&scm) )
{
scheme_deinit(&scm);
//complain violently if it fails to parse
format_log_message(&logmsg, LOGLEVEL_ERROR, "Scheme error loading rules from %s", filename);
if(commhub_fd >= 0) //If we have a connection to the commhub
{
send_message(commhub_fd, &logmsg); //send this driver message to the log
}
return -1;
}
fclose(f);
rules_loaded_flag = 1;
return 0;
}
#ifdef TRACE_NEXT_CALL_OF_FAILED_RULE
char trace_this_rule[RULENAME_LEN + 1] = {0};
#endif
static inline void do_low_level_rulecall(char *rulename)
{
char rulefunc[RULENAME_LEN * 2] = {0};
#ifdef TRACE_NEXT_CALL_OF_FAILED_RULE
if(strcmp(rulename, trace_this_rule)) //if this IS NOT our trace target, process it normally...
{
#endif
//Generate a function call to the full procedure name ("(rule_[whatever_rname_is])");
sprintf(rulefunc, "(rule_%s)", rulename);
//Go and load the string containing the function call in the current context as if it were input from the console
scheme_load_string(&scm, rulefunc);
#ifdef TRACE_NEXT_CALL_OF_FAILED_RULE
if(scheme_error || SCHEME_INTERNAL_ERROR(&scm) ) //if we are in a trace mode, remember that this call incurred errors
{
strncpy(trace_this_rule, rulename, RULENAME_LEN); //and save its rule name, so next time we go
//to execute it, we'll execute it with trace
//enabled...
}
}
else //otherwise, if this IS our trace target, set up tracing and process it with tracing on...
{
#ifdef TRACE_RULE_CALL_TO_FILE
FILE *f;
char trace_dest_name[RULENAME_LEN + 16];
sprintf(trace_dest_name, "/tmp/%s.trace", rulename);
f = fopen(trace_dest_name, "wb");
if(!f)
{
f = stderr;
}
#define TRACE_DEST (f)
#else
#define TRACE_DEST (stderr)
#endif
setlinebuf(TRACE_DEST);
scheme_set_output_port_file(&scm, TRACE_DEST);
fprintf(TRACE_DEST, "---- Start Tracing rule %s ----\n", rulename);
fflush(TRACE_DEST);
scm.tracing = 1;
//Generate a function call to the full procedure name ("(rule_[whatever_rname_is])");
sprintf(rulefunc, "(rule_%s)", rname);
//Go and load the string containing the function call in the current context as if it were input from the console
scheme_load_string(&scm, rulefunc);
scm.tracing = 0;
fprintf(TRACE_DEST, "\n---- Done Tracing rule %s ----\n", rulename);
fflush(TRACE_DEST);
scheme_set_output_port_file(&scm, stdout);
#ifdef TRACE_RULE_CALL_TO_FILE
if(f != stderr)
{
fclose(f);
}
#endif
}
#endif
}
static inline void log_rule_failure()
{
struct message_record foo;
format_log_message(&foo, LOGLEVEL_ERROR, "Failed rule call rule: \"%s\" cred: \"%s\"", rname, cred); //log the failure
if(commhub_fd >= 0) //If we have a connection to the commhub
{
send_message(commhub_fd, &foo); //send this driver message to the log
}
}
int process_driver_rulecall(struct driver_rulecall_struct *drc)
{
if(!drc)
return -1;
// Populate all of our local variables with the values from the NULL rider, except those supplied by the driver's UI
//interaction. These are the variables that the scheme callbacks will use to figure out which rule, credential,
//logical_card_id, sequence_number, etc... applies to this ride.
// The rest of that type of parameter (route, trip, stop, date, time, gps fix, etc...)
//are either calculated on the spot (for date and time), or pulled from the data structures which are updated via IPC by
//the modules responsible for tracking GPS location and paddle data...
rname = drc->rulename;
rparam = drc->ruleparam;
logical_card_id = 0;
seq_num = 0;
cred = "";
// Populate all of our flags which are set inside the scheme callbacks, saying whether we are to accept or reject a rider,
//whether they need to be added to the passback cache, and whether any errors occurred during the processing of the scheme
//rule.
accept_flag = 0;
add_to_passback_flag = 0;
scheme_error = 0;
// This function call actually makes the request of the scheme interpreter to go and load the requested rule. This is
//where the trace wrapping happens, such that a rule that has recently failed will be executed with debug/trace functionality
//enabled for subsequent sessions in the hope that we'll catch an error condition in the diagnostic log that we can use to
//sanely debug the broken rule. (provided the symbol TRACE_NEXT_CALL_OF_FAILED_RULE has been #define'd).
do_low_level_rulecall(rname);
if(scheme_error || SCHEME_INTERNAL_ERROR(&scm))
{
// Log this failure with the offending rule and credential in the diagnostic log
log_rule_failure();
// Also, notify the driver of the rule failure (which should cause the driver to manually accept, and
//hopefully also report the error message to dispatch.
format_driver_message(&user_msg, LOGLEVEL_REJECT, "Rule execution error in: %s", rname);
if(commhub_fd >= 0) //If we have a connection to the commhub
{
send_message(commhub_fd, &user_msg); //send this driver message to the driver
}
return -1;
}
return 0;
}
//int process_rider(passdb_context *ctx, int rider_index, char *credential)
int process_rider(passdb_slim_context *ctx, int rider_index, char *credential)
{
rider_record rr;
int age;
if(!ctx)
return -1;
if(rider_index < 0)
return -1;
//if(rider_index >= NUM_STORED_PASSES)
// return -1;
//if(rider_index >= (2*ctx->hash_modulus) ) //oof...not sure what to do here
// return -1;
//flush the passback cache if it needs it...
apb_flush_if_needed();
make_rider_record( ctx, &rr, rider_index );
//search the anti-passback cache for this credential. If it exists, see how old it is.
//age = apb_lookup(ctx->riders[rider_index].id);
age = apb_lookup(rr.id);
// If it has an age of 0, it isn't in the passback cache. If the age is >= 1, that means that it is age seconds old
//so, effectively, if this credential represents a passback, do the following:
if(age)
{
//Send messages to tell the passenger that they've incurred a passback reject.
format_piu_message(&rider_msg, 1, PIU_PRIORITY_FARE, PASSENGER_MESSAGE_DURATION, "%s %s %ds", REJECT_STR, PASSBACK_STR, age);
//Same for the driver, but we get a longer string...
format_driver_message(&user_msg, LOGLEVEL_REJECT, "Passback (%d sec)", age);
//And similar for the billing log...
//format_billing_message(&bill_msg, REJECT_STR, "PASSBACK", "", "Passback", cred, ctx->riders[rider_index].id, 0);
format_billing_message(&bill_msg, REJECT_STR, "PASSBACK", "", "Passback", cred, rr.id, 0);
if(commhub_fd >= 0) //If we have a connection to the commhub
{
send_message(commhub_fd, &bill_msg); //send the billing message
send_message(commhub_fd, &user_msg); //send the driver message
send_message(commhub_fd, &rider_msg); //send the rider message
}
return 0;
}
// Populate all of our local variables with the values from the selected rider. These are the variables
//that the scheme callbacks will use to figure out which rule, credential, logical_card_id, sequence_number, etc...
//applies to this ride. The rest of that type of parameter (route, trip, stop, date, time, gps fix, etc...)
//are either calculated on the spot (for date and time), or pulled from the data structures which are updated via IPC by
//the modules responsible for tracking GPS location and paddle data...
/*
rname = ctx->riders[rider_index].rule_name;
rparam = ctx->riders[rider_index].rule_param;
logical_card_id = ctx->riders[rider_index].id;
seq_num = ctx->riders[rider_index].seq;
*/
rname = rr.rule_name;
rparam = rr.rule_param;
logical_card_id = rr.id;
seq_num = rr.seq;
cred = credential;
// Populate all of our flags which are set inside the scheme callbacks, saying whether we are to accept or reject a rider,
//whether they need to be added to the passback cache, and whether any errors occurred during the processing of the scheme
//rule.
accept_flag = 0;
add_to_passback_flag = 0;
scheme_error = 0;
// This function call actually makes the request of the scheme interpreter to go and load the requested rule. This is
//where the trace wrapping happens, such that a rule that has recently failed will be executed with debug/trace functionality
//enabled for subsequent sessions in the hope that we'll catch an error condition in the diagnostic log that we can use to
//sanely debug the broken rule. (provided the symbol TRACE_NEXT_CALL_OF_FAILED_RULE has been #define'd).
do_low_level_rulecall(rname);
if(scheme_error || SCHEME_INTERNAL_ERROR(&scm)) //If an scheme interpreter error ocurred while processing a rule
{
// Log this failure with the offending rule and credential in the diagnostic log
log_rule_failure();
// Also, notify the driver of the rule failure (which should cause the driver to manually accept, and
//hopefully also report the error message to dispatch.
format_driver_message(&user_msg, LOGLEVEL_REJECT, "Rule execution error in: %s", rname);
if(commhub_fd >= 0) //If we have a connection to the commhub
{
send_message(commhub_fd, &user_msg); //send this driver message to the driver
}
return -1;
}
if(add_to_passback_flag)
{
//apb_add(ctx->riders[rider_index].id);
apb_add(rr.id);
}
return 0;
}