commit
						3c656db616
					
				
				 24 changed files with 1291 additions and 139 deletions
			
			
		| @ -1 +1 @@ | ||||
| v1.1.1 | ||||
| v1.1.2 | ||||
| @ -0,0 +1,203 @@ | ||||
| #define MAX_BITS_CAN_PACKET (200) | ||||
| 
 | ||||
| // returns out_len
 | ||||
| int do_bitstuff(char *out, char *in, int in_len) { | ||||
|   int last_bit = -1; | ||||
|   int bit_cnt = 0; | ||||
|   int j = 0; | ||||
|   for (int i = 0; i < in_len; i++) { | ||||
|     char bit = in[i]; | ||||
|     out[j++] = bit; | ||||
| 
 | ||||
|     // do the stuffing
 | ||||
|     if (bit == last_bit) { | ||||
|       bit_cnt++; | ||||
|       if (bit_cnt == 5) { | ||||
|         // 5 in a row the same, do stuff
 | ||||
|         last_bit = !bit; | ||||
|         out[j++] = last_bit; | ||||
|         bit_cnt = 1; | ||||
|       } | ||||
|     } else { | ||||
|       // this is a new bit
 | ||||
|       last_bit = bit; | ||||
|       bit_cnt = 1; | ||||
|     } | ||||
|   } | ||||
|   return j; | ||||
| } | ||||
| 
 | ||||
| int append_crc(char *in, int in_len) { | ||||
|   int crc = 0; | ||||
|   for (int i = 0; i < in_len; i++) { | ||||
|     crc <<= 1; | ||||
|     if (in[i] ^ ((crc>>15)&1)) { | ||||
|       crc = crc ^ 0x4599; | ||||
|     } | ||||
|     crc &= 0x7fff; | ||||
|   } | ||||
|   for (int i = 14; i >= 0; i--) { | ||||
|     in[in_len++] = (crc>>i)&1; | ||||
|   } | ||||
|   return in_len; | ||||
| } | ||||
| 
 | ||||
| int append_bits(char *in, int in_len, char *app, int app_len) { | ||||
|   for (int i = 0; i < app_len; i++) { | ||||
|     in[in_len++] = app[i]; | ||||
|   } | ||||
|   return in_len; | ||||
| } | ||||
| 
 | ||||
| int append_int(char *in, int in_len, int val, int val_len) { | ||||
|   for (int i = val_len-1; i >= 0; i--) { | ||||
|     in[in_len++] = (val&(1<<i)) != 0; | ||||
|   } | ||||
|   return in_len; | ||||
| } | ||||
| 
 | ||||
| int get_bit_message(char *out, CAN_FIFOMailBox_TypeDef *to_bang) { | ||||
|   char pkt[MAX_BITS_CAN_PACKET]; | ||||
|   char footer[] = { | ||||
|     1,  // CRC delimiter
 | ||||
|     1,  // ACK
 | ||||
|     1,  // ACK delimiter
 | ||||
|     1,1,1,1,1,1,1, // EOF
 | ||||
|     1,1,1, // IFS
 | ||||
|   }; | ||||
| 
 | ||||
|   int len = 0; | ||||
| 
 | ||||
|   // test packet
 | ||||
|   int dlc_len = to_bang->RDTR & 0xF; | ||||
|   len = append_int(pkt, len, 0, 1);    // Start-of-frame
 | ||||
|   
 | ||||
|   if (to_bang->RIR & 4) { | ||||
|     // extended identifier
 | ||||
|     len = append_int(pkt, len, to_bang->RIR >> 21, 11);  // Identifier
 | ||||
|     len = append_int(pkt, len, 3, 2);    // SRR+IDE
 | ||||
|     len = append_int(pkt, len, (to_bang->RIR >> 3) & ((1<<18)-1), 18);  // Identifier
 | ||||
|     len = append_int(pkt, len, 0, 3);    // RTR+r1+r0
 | ||||
|   } else { | ||||
|     // standard identifier
 | ||||
|     len = append_int(pkt, len, to_bang->RIR >> 21, 11);  // Identifier
 | ||||
|     len = append_int(pkt, len, 0, 3);    // RTR+IDE+reserved
 | ||||
|   } | ||||
| 
 | ||||
|   len = append_int(pkt, len, dlc_len, 4);    // Data length code
 | ||||
| 
 | ||||
|   // append data
 | ||||
|   for (int i = 0; i < dlc_len; i++) { | ||||
|     unsigned char dat = ((unsigned char *)(&(to_bang->RDLR)))[i]; | ||||
|     len = append_int(pkt, len, dat, 8); | ||||
|   } | ||||
| 
 | ||||
|   // append crc
 | ||||
|   len = append_crc(pkt, len); | ||||
| 
 | ||||
|   // do bitstuffing
 | ||||
|   len = do_bitstuff(out, pkt, len); | ||||
| 
 | ||||
|   // append footer
 | ||||
|   len = append_bits(out, len, footer, sizeof(footer)); | ||||
|   return len; | ||||
| } | ||||
| 
 | ||||
| // hardware stuff below this line
 | ||||
| 
 | ||||
| #ifdef PANDA | ||||
| 
 | ||||
| void set_bitbanged_gmlan(int val) { | ||||
|   if (val) { | ||||
|     GPIOB->ODR |= (1 << 13); | ||||
|   } else { | ||||
|     GPIOB->ODR &= ~(1 << 13); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| char pkt_stuffed[MAX_BITS_CAN_PACKET]; | ||||
| int gmlan_sending = -1; | ||||
| int gmlan_sendmax = -1; | ||||
| 
 | ||||
| int gmlan_silent_count = 0; | ||||
| int gmlan_fail_count = 0; | ||||
| #define REQUIRED_SILENT_TIME 10 | ||||
| #define MAX_FAIL_COUNT 10 | ||||
| 
 | ||||
| void TIM4_IRQHandler(void) { | ||||
|   if (TIM4->SR & TIM_SR_UIF && gmlan_sendmax != -1) { | ||||
|     int read = get_gpio_input(GPIOB, 12); | ||||
|     if (gmlan_silent_count < REQUIRED_SILENT_TIME) { | ||||
|       if (read == 0) { | ||||
|         gmlan_silent_count = 0; | ||||
|       } else { | ||||
|         gmlan_silent_count++; | ||||
|       } | ||||
|     } else if (gmlan_silent_count == REQUIRED_SILENT_TIME) { | ||||
|       int retry = 0; | ||||
|       // in send loop
 | ||||
|       if (gmlan_sending > 0 &&  // not first bit
 | ||||
|          (read == 0 && pkt_stuffed[gmlan_sending-1] == 1) &&  // bus wrongly dominant
 | ||||
|          gmlan_sending != (gmlan_sendmax-11)) {    //not ack bit
 | ||||
|         puts("GMLAN ERR: bus driven at "); | ||||
|         puth(gmlan_sending); | ||||
|         puts("\n"); | ||||
|         retry = 1; | ||||
|       } else if (read == 1 && gmlan_sending == (gmlan_sendmax-11)) {    // recessive during ACK
 | ||||
|         puts("GMLAN ERR: didn't recv ACK\n"); | ||||
|         retry = 1; | ||||
|       } | ||||
|       if (retry) { | ||||
|         // reset sender (retry after 7 silent)
 | ||||
|         set_bitbanged_gmlan(1); // recessive
 | ||||
|         gmlan_silent_count = 0; | ||||
|         gmlan_sending = 0; | ||||
|         gmlan_fail_count++; | ||||
|         if (gmlan_fail_count == MAX_FAIL_COUNT) { | ||||
|           puts("GMLAN ERR: giving up send\n"); | ||||
|         } | ||||
|       } else { | ||||
|         set_bitbanged_gmlan(pkt_stuffed[gmlan_sending]); | ||||
|         gmlan_sending++; | ||||
|       } | ||||
|     } | ||||
|     if (gmlan_sending == gmlan_sendmax || gmlan_fail_count == MAX_FAIL_COUNT) { | ||||
|       set_bitbanged_gmlan(1); // recessive
 | ||||
|       set_gpio_mode(GPIOB, 13, MODE_INPUT); | ||||
|       TIM4->DIER = 0;  // no update interrupt
 | ||||
|       TIM4->CR1 = 0;   // disable timer
 | ||||
|       gmlan_sendmax = -1;   // exit
 | ||||
|     } | ||||
|   } | ||||
|   TIM4->SR = 0; | ||||
| } | ||||
| 
 | ||||
| void bitbang_gmlan(CAN_FIFOMailBox_TypeDef *to_bang) { | ||||
|   // TODO: make failure less silent
 | ||||
|   if (gmlan_sendmax != -1) return; | ||||
| 
 | ||||
|   int len = get_bit_message(pkt_stuffed, to_bang); | ||||
|   gmlan_fail_count = 0; | ||||
|   gmlan_silent_count = 0; | ||||
|   gmlan_sending = 0; | ||||
|   gmlan_sendmax = len; | ||||
| 
 | ||||
|   // setup for bitbang loop
 | ||||
|   set_bitbanged_gmlan(1); // recessive
 | ||||
|   set_gpio_mode(GPIOB, 13, MODE_OUTPUT); | ||||
| 
 | ||||
|   // setup
 | ||||
|   TIM4->PSC = 48-1;          // tick on 1 us
 | ||||
|   TIM4->CR1 = TIM_CR1_CEN;   // enable
 | ||||
|   TIM4->ARR = 30-1;          // 33.3 kbps
 | ||||
| 
 | ||||
|   // in case it's disabled
 | ||||
|   NVIC_EnableIRQ(TIM4_IRQn); | ||||
| 
 | ||||
|   // run the interrupt
 | ||||
|   TIM4->DIER = TIM_DIER_UIE; // update interrupt
 | ||||
|   TIM4->SR = 0; | ||||
| } | ||||
| 
 | ||||
| #endif | ||||
| 
 | ||||
| @ -0,0 +1,131 @@ | ||||
| const int CADILLAC_MAX_STEER = 150; // 1s
 | ||||
| // real time torque limit to prevent controls spamming
 | ||||
| // the real time limit is 1500/sec
 | ||||
| const int CADILLAC_MAX_RT_DELTA = 75;       // max delta torque allowed for real time checks
 | ||||
| const int32_t CADILLAC_RT_INTERVAL = 250000;    // 250ms between real time checks
 | ||||
| const int CADILLAC_MAX_RATE_UP = 2; | ||||
| const int CADILLAC_MAX_RATE_DOWN = 5; | ||||
| const int CADILLAC_DRIVER_TORQUE_ALLOWANCE = 50; | ||||
| const int CADILLAC_DRIVER_TORQUE_FACTOR = 4; | ||||
| 
 | ||||
| int cadillac_ign = 0; | ||||
| int cadillac_cruise_engaged_last = 0; | ||||
| int cadillac_rt_torque_last = 0; | ||||
| int cadillac_desired_torque_last[4] = {0};      // 4 torque messages
 | ||||
| uint32_t cadillac_ts_last = 0; | ||||
| int cadillac_supercruise_on = 0; | ||||
| struct sample_t cadillac_torque_driver;         // last few driver torques measured
 | ||||
| 
 | ||||
| int cadillac_get_torque_idx(uint32_t addr) { | ||||
|   if (addr==0x151) return 0; | ||||
|   else if (addr==0x152) return 1; | ||||
|   else if (addr==0x153) return 2; | ||||
|   else return 3; | ||||
| } | ||||
| 
 | ||||
| static void cadillac_rx_hook(CAN_FIFOMailBox_TypeDef *to_push) { | ||||
|   int bus_number = (to_push->RDTR >> 4) & 0xFF; | ||||
|   uint32_t addr = to_push->RIR >> 21; | ||||
| 
 | ||||
|   if (addr == 356) { | ||||
|     int torque_driver_new = ((to_push->RDLR & 0x7) << 8) | ((to_push->RDLR >> 8) & 0xFF); | ||||
|     torque_driver_new = to_signed(torque_driver_new, 11); | ||||
|     // update array of samples
 | ||||
|     update_sample(&cadillac_torque_driver, torque_driver_new); | ||||
|   } | ||||
| 
 | ||||
|   // this message isn't all zeros when ignition is on
 | ||||
|   if (addr == 0x160 && bus_number == 0) { | ||||
|     cadillac_ign = to_push->RDLR > 0; | ||||
|   } | ||||
| 
 | ||||
|   // enter controls on rising edge of ACC, exit controls on ACC off
 | ||||
|   if ((addr == 0x370) && (bus_number == 0)) { | ||||
|     int cruise_engaged = to_push->RDLR & 0x800000;  // bit 23
 | ||||
|     if (cruise_engaged && !cadillac_cruise_engaged_last) { | ||||
|       controls_allowed = 1; | ||||
|     } else if (!cruise_engaged) { | ||||
|       controls_allowed = 0; | ||||
|     } | ||||
|     cadillac_cruise_engaged_last = cruise_engaged; | ||||
|   } | ||||
| 
 | ||||
|   // know supercruise mode and block openpilot msgs if on
 | ||||
|   if ((addr == 0x152) || (addr == 0x154)) { | ||||
|     cadillac_supercruise_on = (to_push->RDHR>>4) & 0x1; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| static int cadillac_tx_hook(CAN_FIFOMailBox_TypeDef *to_send) { | ||||
|   uint32_t addr = to_send->RIR >> 21; | ||||
| 
 | ||||
|   // steer cmd checks
 | ||||
|   if (addr == 0x151 || addr == 0x152 || addr == 0x153 || addr == 0x154) { | ||||
|     int desired_torque = ((to_send->RDLR & 0x3f) << 8) + ((to_send->RDLR & 0xff00) >> 8); | ||||
|     int violation = 0; | ||||
|     uint32_t ts = TIM2->CNT; | ||||
|     int idx = cadillac_get_torque_idx(addr); | ||||
|     desired_torque = to_signed(desired_torque, 14); | ||||
| 
 | ||||
|     if (controls_allowed) { | ||||
| 
 | ||||
|       // *** global torque limit check ***
 | ||||
|       violation |= max_limit_check(desired_torque, CADILLAC_MAX_STEER); | ||||
| 
 | ||||
|       // *** torque rate limit check ***
 | ||||
|       int desired_torque_last = cadillac_desired_torque_last[idx]; | ||||
|       violation |= driver_limit_check(desired_torque, desired_torque_last, &cadillac_torque_driver, | ||||
|         CADILLAC_MAX_STEER, CADILLAC_MAX_RATE_UP, CADILLAC_MAX_RATE_DOWN, | ||||
|         CADILLAC_DRIVER_TORQUE_ALLOWANCE, CADILLAC_DRIVER_TORQUE_FACTOR); | ||||
| 
 | ||||
|       // used next time
 | ||||
|       cadillac_desired_torque_last[idx] = desired_torque; | ||||
| 
 | ||||
|       // *** torque real time rate limit check ***
 | ||||
|       violation |= rt_rate_limit_check(desired_torque, cadillac_rt_torque_last, CADILLAC_MAX_RT_DELTA); | ||||
| 
 | ||||
|       // every RT_INTERVAL set the new limits
 | ||||
|       uint32_t ts_elapsed = get_ts_elapsed(ts, cadillac_ts_last); | ||||
|       if (ts_elapsed > CADILLAC_RT_INTERVAL) { | ||||
|         cadillac_rt_torque_last = desired_torque; | ||||
|         cadillac_ts_last = ts; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // no torque if controls is not allowed
 | ||||
|     if (!controls_allowed && (desired_torque != 0)) { | ||||
|       violation = 1; | ||||
|     } | ||||
| 
 | ||||
|     // reset to 0 if either controls is not allowed or there's a violation
 | ||||
|     if (violation || !controls_allowed) { | ||||
|       cadillac_desired_torque_last[idx] = 0; | ||||
|       cadillac_rt_torque_last = 0; | ||||
|       cadillac_ts_last = ts; | ||||
|     } | ||||
| 
 | ||||
|     if (violation || cadillac_supercruise_on) { | ||||
|       return false; | ||||
|     } | ||||
| 
 | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| static void cadillac_init(int16_t param) { | ||||
|   controls_allowed = 0; | ||||
|   cadillac_ign = 0; | ||||
| } | ||||
| 
 | ||||
| static int cadillac_ign_hook() { | ||||
|   return cadillac_ign; | ||||
| } | ||||
| 
 | ||||
| const safety_hooks cadillac_hooks = { | ||||
|   .init = cadillac_init, | ||||
|   .rx = cadillac_rx_hook, | ||||
|   .tx = cadillac_tx_hook, | ||||
|   .tx_lin = alloutput_tx_lin_hook, | ||||
|   .ignition = cadillac_ign_hook, | ||||
|   .fwd = alloutput_fwd_hook, | ||||
| }; | ||||
| @ -0,0 +1,17 @@ | ||||
| #!/usr/bin/env python | ||||
| import time | ||||
| from panda import Panda | ||||
| 
 | ||||
| p = Panda() | ||||
| p.set_safety_mode(Panda.SAFETY_ALLOUTPUT) | ||||
| p.set_gmlan(bus=2) | ||||
| #p.can_send(0xaaa, "\x00\x00", bus=3) | ||||
| last_add = None | ||||
| while 1: | ||||
|   ret = p.can_recv() | ||||
|   if len(ret) > 0: | ||||
|     add = ret[0][0] | ||||
|     if last_add is not None and add != last_add+1: | ||||
|       print "MISS %d %d" % (last_add, add) | ||||
|     last_add = add | ||||
|     print ret | ||||
| @ -0,0 +1,35 @@ | ||||
| #!/usr/bin/env python | ||||
| import numpy as np | ||||
| import visa | ||||
| import matplotlib.pyplot as plt | ||||
| 
 | ||||
| resources = visa.ResourceManager() | ||||
| print resources.list_resources() | ||||
| 
 | ||||
| scope = resources.open_resource('USB0::0x1AB1::0x04CE::DS1ZA184652242::INSTR', timeout=2000, chunk_size=1024000) | ||||
| print(scope.query('*IDN?').strip()) | ||||
| 
 | ||||
| #voltscale = scope.ask_for_values(':CHAN1:SCAL?')[0] | ||||
| #voltoffset = scope.ask_for_values(":CHAN1:OFFS?")[0] | ||||
| 
 | ||||
| #scope.write(":STOP") | ||||
| scope.write(":WAV:POIN:MODE RAW") | ||||
| scope.write(":WAV:DATA? CHAN1")[10:] | ||||
| rawdata = scope.read_raw() | ||||
| data = np.frombuffer(rawdata, 'B') | ||||
| print data.shape | ||||
| 
 | ||||
| s1 = data[0:650] | ||||
| s2 = data[650:] | ||||
| s1i = np.argmax(s1 > 100) | ||||
| s2i = np.argmax(s2 > 100) | ||||
| s1 = s1[s1i:] | ||||
| s2 = s2[s2i:] | ||||
| 
 | ||||
| plt.plot(s1) | ||||
| plt.plot(s2) | ||||
| plt.show() | ||||
| #data = (data - 130.0 - voltoffset/voltscale*25) / 25 * voltscale | ||||
| 
 | ||||
| print data | ||||
| 
 | ||||
| @ -0,0 +1,33 @@ | ||||
| #!/usr/bin/env python | ||||
| import time | ||||
| from panda import Panda | ||||
| 
 | ||||
| p1 = Panda('380016000551363338383037') | ||||
| p2 = Panda('430026000951363338383037') | ||||
| 
 | ||||
| # this is a test, no safety | ||||
| p1.set_safety_mode(Panda.SAFETY_ALLOUTPUT) | ||||
| p2.set_safety_mode(Panda.SAFETY_ALLOUTPUT) | ||||
| 
 | ||||
| # get versions | ||||
| print(p1.get_version()) | ||||
| print(p2.get_version()) | ||||
| 
 | ||||
| # this sets bus 2 to actually be GMLAN | ||||
| p2.set_gmlan(bus=2) | ||||
| 
 | ||||
| # send w bitbang then without | ||||
| #iden = 123 | ||||
| iden = 18000 | ||||
| #dat = "\x01\x02" | ||||
| dat = "\x01\x02\x03\x04\x05\x06\x07\x08" | ||||
| while 1: | ||||
|   iden += 1 | ||||
|   p1.set_gmlan(bus=None) | ||||
|   p1.can_send(iden, dat, bus=3) | ||||
|   #p1.set_gmlan(bus=2) | ||||
|   #p1.can_send(iden, dat, bus=3) | ||||
|   time.sleep(0.01) | ||||
|   print p2.can_recv() | ||||
|   #exit(0) | ||||
| 
 | ||||
| @ -0,0 +1,23 @@ | ||||
| #!/usr/bin/env python | ||||
| import time | ||||
| from panda import Panda | ||||
| 
 | ||||
| p = Panda() | ||||
| p.set_safety_mode(Panda.SAFETY_ALLOUTPUT) | ||||
| 
 | ||||
| # ack any crap on bus | ||||
| p.set_gmlan(bus=2) | ||||
| time.sleep(0.1) | ||||
| while len(p.can_recv()) > 0: | ||||
|   print "clearing" | ||||
|   time.sleep(0.1) | ||||
| print "cleared" | ||||
| p.set_gmlan(bus=None) | ||||
| 
 | ||||
| iden = 18000 | ||||
| dat = "\x01\x02\x03\x04\x05\x06\x07\x08" | ||||
| while 1: | ||||
|   iden += 1 | ||||
|   p.can_send(iden, dat, bus=3) | ||||
|   time.sleep(0.01) | ||||
| 
 | ||||
| @ -0,0 +1,32 @@ | ||||
| #include <stdio.h> | ||||
| #include <stdint.h> | ||||
| 
 | ||||
| typedef struct { | ||||
|   uint32_t RIR;  /*!< CAN receive FIFO mailbox identifier register */ | ||||
|   uint32_t RDTR; /*!< CAN receive FIFO mailbox data length control and time stamp register */ | ||||
|   uint32_t RDLR; /*!< CAN receive FIFO mailbox data low register */ | ||||
|   uint32_t RDHR; /*!< CAN receive FIFO mailbox data high register */ | ||||
| } CAN_FIFOMailBox_TypeDef; | ||||
| 
 | ||||
| #include "../../board/drivers/canbitbang.h" | ||||
| 
 | ||||
| int main() { | ||||
|   char out[300]; | ||||
|   CAN_FIFOMailBox_TypeDef to_bang = {0}; | ||||
|   to_bang.RIR = 20 << 21; | ||||
|   to_bang.RDTR = 1; | ||||
|   to_bang.RDLR = 1; | ||||
| 
 | ||||
|   int len = get_bit_message(out, &to_bang); | ||||
|   printf("T:"); | ||||
|   for (int i = 0; i < len; i++) { | ||||
|     printf("%d", out[i]); | ||||
|   } | ||||
|   printf("\n"); | ||||
|   printf("R:0000010010100000100010000010011110111010100111111111111111"); | ||||
|   printf("\n"); | ||||
|   return 0; | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @ -0,0 +1,182 @@ | ||||
| #!/usr/bin/env python2 | ||||
| import unittest | ||||
| import numpy as np | ||||
| import libpandasafety_py | ||||
| 
 | ||||
| MAX_RATE_UP = 2 | ||||
| MAX_RATE_DOWN = 5 | ||||
| MAX_TORQUE = 150 | ||||
| 
 | ||||
| MAX_RT_DELTA = 75 | ||||
| RT_INTERVAL = 250000 | ||||
| 
 | ||||
| DRIVER_TORQUE_ALLOWANCE = 50; | ||||
| DRIVER_TORQUE_FACTOR = 4; | ||||
| 
 | ||||
| IPAS_OVERRIDE_THRESHOLD = 200 | ||||
| 
 | ||||
| def twos_comp(val, bits): | ||||
|   if val >= 0: | ||||
|     return val | ||||
|   else: | ||||
|     return (2**bits) + val | ||||
| 
 | ||||
| def sign(a): | ||||
|   if a > 0: | ||||
|     return 1 | ||||
|   else: | ||||
|     return -1 | ||||
| 
 | ||||
| class TestCadillacSafety(unittest.TestCase): | ||||
|   @classmethod | ||||
|   def setUp(cls): | ||||
|     cls.safety = libpandasafety_py.libpandasafety | ||||
|     cls.safety.cadillac_init(0) | ||||
|     cls.safety.init_tests_cadillac() | ||||
| 
 | ||||
|   def _set_prev_torque(self, t): | ||||
|     self.safety.set_cadillac_desired_torque_last(t) | ||||
|     self.safety.set_cadillac_rt_torque_last(t) | ||||
| 
 | ||||
|   def _torque_driver_msg(self, torque): | ||||
|     to_send = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_send[0].RIR = 0x164 << 21 | ||||
| 
 | ||||
|     t = twos_comp(torque, 11) | ||||
|     to_send[0].RDLR = ((t >> 8) & 0x7) | ((t & 0xFF) << 8) | ||||
|     return to_send | ||||
| 
 | ||||
|   def _torque_driver_msg_array(self, torque): | ||||
|     for i in range(3): | ||||
|       self.safety.cadillac_ipas_rx_hook(self._torque_driver_msg(torque)) | ||||
| 
 | ||||
|   def _torque_msg(self, torque): | ||||
|     to_send = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_send[0].RIR = 0x151 << 21 | ||||
| 
 | ||||
|     t = twos_comp(torque, 14) | ||||
|     to_send[0].RDLR = ((t >> 8) & 0x3F) | ((t & 0xFF) << 8) | ||||
|     return to_send | ||||
| 
 | ||||
|   def test_default_controls_not_allowed(self): | ||||
|     self.assertFalse(self.safety.get_controls_allowed()) | ||||
| 
 | ||||
|   def test_manually_enable_controls_allowed(self): | ||||
|     self.safety.set_controls_allowed(1) | ||||
|     self.assertTrue(self.safety.get_controls_allowed()) | ||||
|     self.safety.set_controls_allowed(0) | ||||
| 
 | ||||
|   def test_enable_control_allowed_from_cruise(self): | ||||
|     to_push = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_push[0].RIR = 0x370 << 21 | ||||
|     to_push[0].RDLR = 0x800000 | ||||
|     to_push[0].RDTR = 0 | ||||
| 
 | ||||
|     self.safety.cadillac_rx_hook(to_push) | ||||
|     self.assertTrue(self.safety.get_controls_allowed()) | ||||
| 
 | ||||
|   def test_disable_control_allowed_from_cruise(self): | ||||
|     to_push = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_push[0].RIR = 0x370 << 21 | ||||
|     to_push[0].RDLR = 0 | ||||
|     to_push[0].RDTR = 0 | ||||
| 
 | ||||
|     self.safety.set_controls_allowed(1) | ||||
|     self.safety.cadillac_rx_hook(to_push) | ||||
|     self.assertFalse(self.safety.get_controls_allowed()) | ||||
| 
 | ||||
|   def test_torque_absolute_limits(self): | ||||
|     for controls_allowed in [True, False]: | ||||
|       for torque in np.arange(-MAX_TORQUE - 1000, MAX_TORQUE + 1000, MAX_RATE_UP): | ||||
|         self.safety.set_controls_allowed(controls_allowed) | ||||
|         self.safety.set_cadillac_rt_torque_last(torque) | ||||
|         self.safety.set_cadillac_torque_driver(0, 0) | ||||
|         self.safety.set_cadillac_desired_torque_last(torque - MAX_RATE_UP) | ||||
| 
 | ||||
|         if controls_allowed: | ||||
|           send = (-MAX_TORQUE <= torque <= MAX_TORQUE) | ||||
|         else: | ||||
|           send = torque == 0 | ||||
| 
 | ||||
|         self.assertEqual(send, self.safety.cadillac_tx_hook(self._torque_msg(torque))) | ||||
| 
 | ||||
|   def test_non_realtime_limit_up(self): | ||||
|     self.safety.set_cadillac_torque_driver(0, 0) | ||||
|     self.safety.set_controls_allowed(True) | ||||
| 
 | ||||
|     self._set_prev_torque(0) | ||||
|     self.assertTrue(self.safety.cadillac_tx_hook(self._torque_msg(MAX_RATE_UP))) | ||||
|     self._set_prev_torque(0) | ||||
|     self.assertTrue(self.safety.cadillac_tx_hook(self._torque_msg(-MAX_RATE_UP))) | ||||
| 
 | ||||
|     self._set_prev_torque(0) | ||||
|     self.assertFalse(self.safety.cadillac_tx_hook(self._torque_msg(MAX_RATE_UP + 1))) | ||||
|     self.safety.set_controls_allowed(True) | ||||
|     self._set_prev_torque(0) | ||||
|     self.assertFalse(self.safety.cadillac_tx_hook(self._torque_msg(-MAX_RATE_UP - 1))) | ||||
| 
 | ||||
|   def test_non_realtime_limit_down(self): | ||||
|     self.safety.set_cadillac_torque_driver(0, 0) | ||||
|     self.safety.set_controls_allowed(True) | ||||
| 
 | ||||
|   def test_exceed_torque_sensor(self): | ||||
|     self.safety.set_controls_allowed(True) | ||||
| 
 | ||||
|     for sign in [-1, 1]: | ||||
|       for t in np.arange(0, DRIVER_TORQUE_ALLOWANCE + 1, 1): | ||||
|         t *= -sign | ||||
|         self.safety.set_cadillac_torque_driver(t, t) | ||||
|         self._set_prev_torque(MAX_TORQUE * sign) | ||||
|         self.assertTrue(self.safety.cadillac_tx_hook(self._torque_msg(MAX_TORQUE * sign))) | ||||
| 
 | ||||
|       self.safety.set_cadillac_torque_driver(DRIVER_TORQUE_ALLOWANCE + 1, DRIVER_TORQUE_ALLOWANCE + 1) | ||||
|       self.assertFalse(self.safety.cadillac_tx_hook(self._torque_msg(-MAX_TORQUE))) | ||||
| 
 | ||||
|     # spot check some individual cases | ||||
|     for sign in [-1, 1]: | ||||
|       driver_torque = (DRIVER_TORQUE_ALLOWANCE + 10) * sign | ||||
|       torque_desired = (MAX_TORQUE - 10 * DRIVER_TORQUE_FACTOR) * sign | ||||
|       delta = 1 * sign | ||||
|       self._set_prev_torque(torque_desired) | ||||
|       self.safety.set_cadillac_torque_driver(-driver_torque, -driver_torque) | ||||
|       self.assertTrue(self.safety.cadillac_tx_hook(self._torque_msg(torque_desired))) | ||||
|       self._set_prev_torque(torque_desired + delta) | ||||
|       self.safety.set_cadillac_torque_driver(-driver_torque, -driver_torque) | ||||
|       self.assertFalse(self.safety.cadillac_tx_hook(self._torque_msg(torque_desired + delta))) | ||||
| 
 | ||||
|       self._set_prev_torque(MAX_TORQUE * sign) | ||||
|       self.safety.set_cadillac_torque_driver(-MAX_TORQUE * sign, -MAX_TORQUE * sign) | ||||
|       self.assertTrue(self.safety.cadillac_tx_hook(self._torque_msg((MAX_TORQUE - MAX_RATE_DOWN) * sign))) | ||||
|       self._set_prev_torque(MAX_TORQUE * sign) | ||||
|       self.safety.set_cadillac_torque_driver(-MAX_TORQUE * sign, -MAX_TORQUE * sign) | ||||
|       self.assertTrue(self.safety.cadillac_tx_hook(self._torque_msg(0))) | ||||
|       self._set_prev_torque(MAX_TORQUE * sign) | ||||
|       self.safety.set_cadillac_torque_driver(-MAX_TORQUE * sign, -MAX_TORQUE * sign) | ||||
|       self.assertFalse(self.safety.cadillac_tx_hook(self._torque_msg((MAX_TORQUE - MAX_RATE_DOWN + 1) * sign))) | ||||
| 
 | ||||
| 
 | ||||
|   def test_realtime_limits(self): | ||||
|     self.safety.set_controls_allowed(True) | ||||
| 
 | ||||
|     for sign in [-1, 1]: | ||||
|       self.safety.init_tests_cadillac() | ||||
|       self._set_prev_torque(0) | ||||
|       self.safety.set_cadillac_torque_driver(0, 0) | ||||
|       for t in np.arange(0, MAX_RT_DELTA, 1): | ||||
|         t *= sign | ||||
|         self.assertTrue(self.safety.cadillac_tx_hook(self._torque_msg(t))) | ||||
|       self.assertFalse(self.safety.cadillac_tx_hook(self._torque_msg(sign * (MAX_RT_DELTA + 1)))) | ||||
| 
 | ||||
|       self._set_prev_torque(0) | ||||
|       for t in np.arange(0, MAX_RT_DELTA, 1): | ||||
|         t *= sign | ||||
|         self.assertTrue(self.safety.cadillac_tx_hook(self._torque_msg(t))) | ||||
| 
 | ||||
|       # Increase timer to update rt_torque_last | ||||
|       self.safety.set_timer(RT_INTERVAL + 1) | ||||
|       self.assertTrue(self.safety.cadillac_tx_hook(self._torque_msg(sign * (MAX_RT_DELTA - 1)))) | ||||
|       self.assertTrue(self.safety.cadillac_tx_hook(self._torque_msg(sign * (MAX_RT_DELTA + 1)))) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   unittest.main() | ||||
| @ -0,0 +1,270 @@ | ||||
| #!/usr/bin/env python2 | ||||
| import unittest | ||||
| import numpy as np | ||||
| import libpandasafety_py | ||||
| 
 | ||||
| MAX_RATE_UP = 7 | ||||
| MAX_RATE_DOWN = 17 | ||||
| MAX_STEER = 255 | ||||
| MAX_BRAKE = 350 | ||||
| MAX_GAS = 3072 | ||||
| MAX_REGEN = 1404 | ||||
| 
 | ||||
| MAX_RT_DELTA = 128 | ||||
| RT_INTERVAL = 250000 | ||||
| 
 | ||||
| DRIVER_TORQUE_ALLOWANCE = 50; | ||||
| DRIVER_TORQUE_FACTOR = 4; | ||||
| 
 | ||||
| def twos_comp(val, bits): | ||||
|   if val >= 0: | ||||
|     return val | ||||
|   else: | ||||
|     return (2**bits) + val | ||||
| 
 | ||||
| def sign(a): | ||||
|   if a > 0: | ||||
|     return 1 | ||||
|   else: | ||||
|     return -1 | ||||
| 
 | ||||
| class TestGmSafety(unittest.TestCase): | ||||
|   @classmethod | ||||
|   def setUp(cls): | ||||
|     cls.safety = libpandasafety_py.libpandasafety | ||||
|     cls.safety.gm_init(0) | ||||
|     cls.safety.init_tests_gm() | ||||
| 
 | ||||
|   def _speed_msg(self, speed): | ||||
|     to_send = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_send[0].RIR = 842 << 21 | ||||
|     to_send[0].RDLR = speed | ||||
|     return to_send | ||||
| 
 | ||||
|   def _button_msg(self, buttons): | ||||
|     to_send = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_send[0].RIR = 481 << 21 | ||||
|     to_send[0].RDHR = buttons << 12 | ||||
|     return to_send | ||||
| 
 | ||||
|   def _brake_msg(self, brake): | ||||
|     to_send = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_send[0].RIR = 241 << 21 | ||||
|     to_send[0].RDLR = 0xa00 if brake else 0x900 | ||||
|     return to_send | ||||
| 
 | ||||
|   def _gas_msg(self, gas): | ||||
|     to_send = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_send[0].RIR = 417 << 21 | ||||
|     to_send[0].RDHR = (1 << 16) if gas else 0 | ||||
|     return to_send | ||||
| 
 | ||||
|   def _send_brake_msg(self, brake): | ||||
|     to_send = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_send[0].RIR = 789 << 21 | ||||
|     brake = (-brake) & 0xfff | ||||
|     to_send[0].RDLR = (brake >> 8) | ((brake &0xff) << 8) | ||||
|     return to_send | ||||
| 
 | ||||
|   def _send_gas_msg(self, gas): | ||||
|     to_send = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_send[0].RIR = 715 << 21 | ||||
|     to_send[0].RDLR = ((gas & 0x1f) << 27) | ((gas & 0xfe0) << 11) | ||||
|     return to_send | ||||
| 
 | ||||
|   def _set_prev_torque(self, t): | ||||
|     self.safety.set_gm_desired_torque_last(t) | ||||
|     self.safety.set_gm_rt_torque_last(t) | ||||
| 
 | ||||
|   def _torque_driver_msg(self, torque): | ||||
|     to_send = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_send[0].RIR = 388 << 21 | ||||
| 
 | ||||
|     t = twos_comp(torque, 11) | ||||
|     to_send[0].RDHR = (((t >> 8) & 0x7) << 16) | ((t & 0xFF) << 24) | ||||
|     return to_send | ||||
| 
 | ||||
|   def _torque_driver_msg_array(self, torque): | ||||
|     for i in range(3): | ||||
|       self.safety.gm_ipas_rx_hook(self._torque_driver_msg(torque)) | ||||
| 
 | ||||
|   def _torque_msg(self, torque): | ||||
|     to_send = libpandasafety_py.ffi.new('CAN_FIFOMailBox_TypeDef *') | ||||
|     to_send[0].RIR = 384 << 21 | ||||
| 
 | ||||
|     t = twos_comp(torque, 11) | ||||
|     to_send[0].RDLR = ((t >> 8) & 0x7) | ((t & 0xFF) << 8) | ||||
|     return to_send | ||||
| 
 | ||||
|   def test_default_controls_not_allowed(self): | ||||
|     self.assertFalse(self.safety.get_controls_allowed()) | ||||
| 
 | ||||
|   def test_resume_button(self): | ||||
|     RESUME_BTN = 2 | ||||
|     self.safety.set_controls_allowed(0) | ||||
|     self.safety.gm_rx_hook(self._button_msg(RESUME_BTN)) | ||||
|     self.assertTrue(self.safety.get_controls_allowed()) | ||||
| 
 | ||||
|   def test_set_button(self): | ||||
|     SET_BTN = 3 | ||||
|     self.safety.set_controls_allowed(0) | ||||
|     self.safety.gm_rx_hook(self._button_msg(SET_BTN)) | ||||
|     self.assertTrue(self.safety.get_controls_allowed()) | ||||
| 
 | ||||
|   def test_cancel_button(self): | ||||
|     CANCEL_BTN = 6 | ||||
|     self.safety.set_controls_allowed(1) | ||||
|     self.safety.gm_rx_hook(self._button_msg(CANCEL_BTN)) | ||||
|     self.assertFalse(self.safety.get_controls_allowed()) | ||||
| 
 | ||||
|   def test_disengage_on_brake(self):  | ||||
|     self.safety.set_controls_allowed(1) | ||||
|     self.safety.gm_rx_hook(self._brake_msg(True)) | ||||
|     self.assertFalse(self.safety.get_controls_allowed()) | ||||
| 
 | ||||
|   def test_allow_brake_at_zero_speed(self): | ||||
|     # Brake was already pressed | ||||
|     self.safety.gm_rx_hook(self._brake_msg(True)) | ||||
|     self.safety.set_controls_allowed(1) | ||||
| 
 | ||||
|     self.safety.gm_rx_hook(self._brake_msg(True)) | ||||
|     self.assertTrue(self.safety.get_controls_allowed()) | ||||
|     self.safety.gm_rx_hook(self._brake_msg(False)) | ||||
| 
 | ||||
|   def test_not_allow_brake_when_moving(self): | ||||
|     # Brake was already pressed | ||||
|     self.safety.gm_rx_hook(self._brake_msg(True)) | ||||
|     self.safety.gm_rx_hook(self._speed_msg(100)) | ||||
|     self.safety.set_controls_allowed(1) | ||||
| 
 | ||||
|     self.safety.gm_rx_hook(self._brake_msg(True)) | ||||
|     self.assertFalse(self.safety.get_controls_allowed()) | ||||
|     self.safety.gm_rx_hook(self._brake_msg(False)) | ||||
| 
 | ||||
|   def test_disengage_on_gas(self): | ||||
|     self.safety.set_controls_allowed(1) | ||||
|     self.safety.gm_rx_hook(self._gas_msg(True)) | ||||
|     self.assertFalse(self.safety.get_controls_allowed()) | ||||
|     self.safety.gm_rx_hook(self._gas_msg(False)) | ||||
| 
 | ||||
|   def test_allow_engage_with_gas_pressed(self): | ||||
|     self.safety.gm_rx_hook(self._gas_msg(True)) | ||||
|     self.safety.set_controls_allowed(1) | ||||
|     self.safety.gm_rx_hook(self._gas_msg(True)) | ||||
|     self.assertTrue(self.safety.get_controls_allowed()) | ||||
|     self.safety.gm_rx_hook(self._gas_msg(False)) | ||||
| 
 | ||||
|   def test_brake_safety_check(self): | ||||
|     for enabled in [0, 1]: | ||||
|       for b in range(0, 500): | ||||
|         self.safety.set_controls_allowed(enabled) | ||||
|         if abs(b) > MAX_BRAKE or (not enabled and b != 0): | ||||
|           self.assertFalse(self.safety.gm_tx_hook(self._send_brake_msg(b))) | ||||
|         else: | ||||
|           self.assertTrue(self.safety.gm_tx_hook(self._send_brake_msg(b))) | ||||
| 
 | ||||
|   def test_gas_safety_check(self): | ||||
|     for enabled in [0, 1]: | ||||
|       for g in range(0, 2**12-1): | ||||
|         self.safety.set_controls_allowed(enabled) | ||||
|         if abs(g) > MAX_GAS or (not enabled and g != MAX_REGEN): | ||||
|           self.assertFalse(self.safety.gm_tx_hook(self._send_gas_msg(g))) | ||||
|         else: | ||||
|           self.assertTrue(self.safety.gm_tx_hook(self._send_gas_msg(g))) | ||||
| 
 | ||||
|   def test_steer_safety_check(self): | ||||
|     for enabled in [0, 1]: | ||||
|       for t in range(-0x200, 0x200): | ||||
|         self.safety.set_controls_allowed(enabled) | ||||
|         self._set_prev_torque(t) | ||||
|         if abs(t) > MAX_STEER or (not enabled and abs(t) > 0): | ||||
|           self.assertFalse(self.safety.gm_tx_hook(self._torque_msg(t))) | ||||
|         else: | ||||
|           self.assertTrue(self.safety.gm_tx_hook(self._torque_msg(t))) | ||||
| 
 | ||||
|   def test_manually_enable_controls_allowed(self): | ||||
|     self.safety.set_controls_allowed(1) | ||||
|     self.assertTrue(self.safety.get_controls_allowed()) | ||||
|     self.safety.set_controls_allowed(0) | ||||
|     self.assertFalse(self.safety.get_controls_allowed()) | ||||
| 
 | ||||
|   def test_non_realtime_limit_up(self): | ||||
|     self.safety.set_gm_torque_driver(0, 0) | ||||
|     self.safety.set_controls_allowed(True) | ||||
| 
 | ||||
|     self._set_prev_torque(0) | ||||
|     self.assertTrue(self.safety.gm_tx_hook(self._torque_msg(MAX_RATE_UP))) | ||||
|     self._set_prev_torque(0) | ||||
|     self.assertTrue(self.safety.gm_tx_hook(self._torque_msg(-MAX_RATE_UP))) | ||||
| 
 | ||||
|     self._set_prev_torque(0) | ||||
|     self.assertFalse(self.safety.gm_tx_hook(self._torque_msg(MAX_RATE_UP + 1))) | ||||
|     self.safety.set_controls_allowed(True) | ||||
|     self._set_prev_torque(0) | ||||
|     self.assertFalse(self.safety.gm_tx_hook(self._torque_msg(-MAX_RATE_UP - 1))) | ||||
| 
 | ||||
|   def test_non_realtime_limit_down(self): | ||||
|     self.safety.set_gm_torque_driver(0, 0) | ||||
|     self.safety.set_controls_allowed(True) | ||||
| 
 | ||||
|   def test_against_torque_driver(self): | ||||
|     self.safety.set_controls_allowed(True) | ||||
| 
 | ||||
|     for sign in [-1, 1]: | ||||
|       for t in np.arange(0, DRIVER_TORQUE_ALLOWANCE + 1, 1): | ||||
|         t *= -sign | ||||
|         self.safety.set_gm_torque_driver(t, t) | ||||
|         self._set_prev_torque(MAX_STEER * sign) | ||||
|         self.assertTrue(self.safety.gm_tx_hook(self._torque_msg(MAX_STEER * sign))) | ||||
| 
 | ||||
|       self.safety.set_gm_torque_driver(DRIVER_TORQUE_ALLOWANCE + 1, DRIVER_TORQUE_ALLOWANCE + 1) | ||||
|       self.assertFalse(self.safety.gm_tx_hook(self._torque_msg(-MAX_STEER))) | ||||
| 
 | ||||
|     # spot check some individual cases | ||||
|     for sign in [-1, 1]: | ||||
|       driver_torque = (DRIVER_TORQUE_ALLOWANCE + 10) * sign | ||||
|       torque_desired = (MAX_STEER - 10 * DRIVER_TORQUE_FACTOR) * sign | ||||
|       delta = 1 * sign | ||||
|       self._set_prev_torque(torque_desired) | ||||
|       self.safety.set_gm_torque_driver(-driver_torque, -driver_torque) | ||||
|       self.assertTrue(self.safety.gm_tx_hook(self._torque_msg(torque_desired))) | ||||
|       self._set_prev_torque(torque_desired + delta) | ||||
|       self.safety.set_gm_torque_driver(-driver_torque, -driver_torque) | ||||
|       self.assertFalse(self.safety.gm_tx_hook(self._torque_msg(torque_desired + delta))) | ||||
| 
 | ||||
|       self._set_prev_torque(MAX_STEER * sign) | ||||
|       self.safety.set_gm_torque_driver(-MAX_STEER * sign, -MAX_STEER * sign) | ||||
|       self.assertTrue(self.safety.gm_tx_hook(self._torque_msg((MAX_STEER - MAX_RATE_DOWN) * sign))) | ||||
|       self._set_prev_torque(MAX_STEER * sign) | ||||
|       self.safety.set_gm_torque_driver(-MAX_STEER * sign, -MAX_STEER * sign) | ||||
|       self.assertTrue(self.safety.gm_tx_hook(self._torque_msg(0))) | ||||
|       self._set_prev_torque(MAX_STEER * sign) | ||||
|       self.safety.set_gm_torque_driver(-MAX_STEER * sign, -MAX_STEER * sign) | ||||
|       self.assertFalse(self.safety.gm_tx_hook(self._torque_msg((MAX_STEER - MAX_RATE_DOWN + 1) * sign))) | ||||
| 
 | ||||
| 
 | ||||
|   def test_realtime_limits(self): | ||||
|     self.safety.set_controls_allowed(True) | ||||
| 
 | ||||
|     for sign in [-1, 1]: | ||||
|       self.safety.init_tests_gm() | ||||
|       self._set_prev_torque(0) | ||||
|       self.safety.set_gm_torque_driver(0, 0) | ||||
|       for t in np.arange(0, MAX_RT_DELTA, 1): | ||||
|         t *= sign | ||||
|         self.assertTrue(self.safety.gm_tx_hook(self._torque_msg(t))) | ||||
|       self.assertFalse(self.safety.gm_tx_hook(self._torque_msg(sign * (MAX_RT_DELTA + 1)))) | ||||
| 
 | ||||
|       self._set_prev_torque(0) | ||||
|       for t in np.arange(0, MAX_RT_DELTA, 1): | ||||
|         t *= sign | ||||
|         self.assertTrue(self.safety.gm_tx_hook(self._torque_msg(t))) | ||||
| 
 | ||||
|       # Increase timer to update rt_torque_last | ||||
|       self.safety.set_timer(RT_INTERVAL + 1) | ||||
|       self.assertTrue(self.safety.gm_tx_hook(self._torque_msg(sign * (MAX_RT_DELTA - 1)))) | ||||
|       self.assertTrue(self.safety.gm_tx_hook(self._torque_msg(sign * (MAX_RT_DELTA + 1)))) | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|   unittest.main() | ||||
					Loading…
					
					
				
		Reference in new issue