Project

General

Profile

Revision 116

Updated recharging code for the robot, it actually runs, trying to fix problem with orbs in debugging mode.

View differences:

branches/autonomous_recharging/code/projects/autonomous_recharging/charging_station/xbee.c
165 165
		exit(0);
166 166
	}
167 167
	#endif
168
	usb_puts("Entering command mode.\n");
168 169
	xbee_enter_command_mode();
170
	usb_puts("Entered command mode.\n");
169 171
	xbee_enter_api_mode();
170 172
	xbee_exit_command_mode();
171 173
	xbee_send_read_at_command("MY");
branches/autonomous_recharging/code/projects/autonomous_recharging/charging_station/charging_defs.h
12 12

  
13 13
#ifdef STATION_DEBUG
14 14
#define STATION_DEBUG_PRINT( s ) usb_puts( s )
15
#define STATION_DEBUG_PUTI( i ) usb_puti( i )
15 16
#else
16 17
#define STATION_DEBUG_PRINT( s )
18
#define STATION_DEBUG_PUTI( i )
17 19
#endif
18 20

  
19 21
/**
branches/autonomous_recharging/code/projects/autonomous_recharging/charging_station/Makefile
202 202
AVRDUDE_PROGRAMMER = avrisp
203 203

  
204 204
# com1 = serial port. Use lpt1 to connect to parallel port.
205
AVRDUDE_PORT = /dev/ttyUSB0
205
AVRDUDE_PORT = /dev/ttyUSB1
206 206
# programmer connected to serial device
207 207

  
208 208
AVRDUDE_WRITE_FLASH = -b 9600 -U flash:w:$(TARGET).hex
branches/autonomous_recharging/code/projects/autonomous_recharging/charging_station/bays.c
1 1
#include "firefly+_lib.h"
2 2
#include "bays.h"
3 3

  
4
#define MAX(a,b) ((a) > (b) ? (a) : (b))
5
#define MIN(a,b) ((a) < (b) ? (a) : (b))
6

  
7

  
8
void bay_tester(void)
9
{
10
    bayStates[1] = 50;
11
    bayStates[2] = 51;
12
    bayStates[3] = 52;
13
}
14

  
15
void print_bays_debug(void)
16
{
17

  
18
	usb_puts("\n\r");
19
	usb_puts("Bays: ");
20
	for(int i = 0; i < NUM_BAYS; i++) {
21
		usb_puti(bayStates[i]);
22
		usb_putc(' ');
23
	}
24
	usb_puts("\n\r");
25
	
26
	usb_puts("Seeking robot: ");
27
	usb_puti(robot_seeking);
28
	usb_puts("\n\r");
29
}
30

  
31
void do_consistency_checks(void)
32
{
33
    if(robot_seeking != 255 && robot_assignment(robot_seeking) == 255) // the only free bay from bay_tester()
34
    {
35
      usb_puts("BAD STUFF HAPPENED...\n\r");
36
      usb_puts("robot assigned to BAY 1 does not equal robot_seeking\n\r");
37
      usb_puts("\n\rrobot_seeking: ");
38
      usb_puti(robot_seeking);
39
      usb_puts("Bay 1 contains:  ");
40
      usb_puti(bayStates[1]);
41
      usb_puts("\n\r");
42
      //while(1);
43
    }
44
}
45
// END DEBUGGING FUNCTIONS
46

  
47 4
void bays_init(void)
48 5
{
49
	int i;
50
	for (i = 0; i < NUM_BAYS; i++)
51
		bayStates[i] = 255;
52
	robot_seeking = 255;
53
	bay_tester();
54
	
55 6
	digital_output(ENABLE_PIN, 0);
56 7
	digital_output(BOM_PIN, 1);
57 8
	digital_output(SELECT_0, 0);
......
61 12
	active_bay = -1;
62 13
}
63 14

  
64
/*int assign_bay(int robot)
65
{
66
	int max_bom = sensors[robot]; // where the station sees the robot (max bom)
67
	int closest = 255;
68
	int distance = 16;
69
	int i;
70
	for (i = 0; i < NUM_BAYS; i++)
71
	{
72
		if (bayStates[i] != 255)
73
			continue;
74
		int d = max_bom - i * 4; // distance (BOM-wise) from bay
75
		if (d < -8)
76
			d += 16;
77
		else
78
			if (d > 8)
79
				d -= 16;
80
		if (d < 0)
81
			d *= -1;
82
		if (d < distance) // finds closest bay to robot's current position
83
		{
84
			distance = d; // distance (BOM-wise)
85
			closest = i; // actual bay number
86
		}
87
	}
88
	if (closest == 255) // all full
89
		return 255;
90
	bayStates[closest] = robot;
91
	return closest;
92
}*/
93

  
94
/*int assign_bay(int robot) // returns number of an empty bay (255 if all full)
95
{
96
    for(int i = 0; i < NUM_BAYS; i++) // right now, go thru linearly and allocate first available bay
97
    {
98
        if(bayStates[i] == 255)
99
        {
100
            bayStates[i] = robot;
101
            return i;
102
        }
103
    }
104
}*/
105

  
106
int left_index( int pos )
107
{
108
	int i;
109
	for(i = pos; i >= 0; i--)
110
	{
111
		if(bayStates[i] != 255)
112
			return i;
113
	}
114
	return -1;
115
}
116

  
117
int right_index( int pos )
118
{
119
	int i;
120
	for(i = pos; i < NUM_BAYS; i++)
121
	{
122
		if(bayStates[i] != 255)
123
			return i;
124
	}
125
	return NUM_BAYS;
126
}
127

  
128
/*int assign_bay( int robot ) // 'smart' assign_bay()
129
{
130
	int ldist = 0;
131
	int rdist = 0;
132
	int score = 0;
133
	int max = 0;
134
	
135
	int bay = 255;
136
	
137
	int count = 0;
138
	
139
	int i;
140
	for(i = 0; i < NUM_BAYS; i++)
141
	{
142
		if(bayStates[i] == 255)
143
			count++;
144
	}
145
	if(count == NUM_BAYS)
146
	{
147
		bayStates[0] = robot;
148
		return 0;
149
	}
150
	
151
	for(i = 0; i < NUM_BAYS; i++)
152
	{
153
		if(bayStates[i] == 255)
154
		{
155
			ldist = i - left_index(i);
156
			rdist = right_index(i) - i;
157
			if(ldist - i == 1)
158
				score = rdist;
159
			else if(i + rdist == NUM_BAYS)
160
				score = ldist;
161
			else
162
				score = MIN(ldist,rdist);
163
			if(score > max)
164
			{
165
				max = score;
166
				bay = i;
167
			}
168
		}
169
		ldist = rdist = 0;
170
	}
171
	bayStates[bay] = robot;
172
	return bay;
173
}*/
174

  
175
int robot_assignment(int robot) //returns bay to which 'robot' has been assigned, 255 if not yet assigned
176
{
177
    for(int i = 0; i < NUM_BAYS; i++)
178
    {
179
        if(bayStates[i] == robot)
180
            return i;
181
    }
182
    return 255;
183
}
184

  
185
int is_empty(void) //returns 1 if there is an empty bay, 0 if not
186
{
187
    for(int i = 0; i < NUM_BAYS; i++)
188
    {
189
        if(bayStates[i] == 255)
190
            return 1;
191
    }
192
    return 0;
193
}
194

  
195 15
void lbom_on(void)
196 16
{
197 17
	digital_output(BOM_PIN, 0);
......
217 37
{
218 38
	digital_output(ENABLE_PIN, 0);
219 39
}
40

  
branches/autonomous_recharging/code/projects/autonomous_recharging/charging_station/bays.h
7 7
#define SELECT_1	_PIN_A3
8 8
#define SELECT_2	_PIN_A4
9 9

  
10
#define NUM_BAYS 4
11

  
12
#define MAX_ROBOTS 20
13
char sensors[MAX_ROBOTS]; //from wl_charging_station...probably should be there instead of here, but assign_bay needs for now
14

  
15
int bayStates[NUM_BAYS]; //holds robot id of robot in each bay 0-3 (255 if empty)
16
int robot_seeking; //robot if of the robot currently seeking (255 if none)
17

  
18 10
int active_bay;
19 11

  
20
void bay_tester(void);
21
void print_bays_debug(void);
22
void do_consistency_checks(void);
23

  
12
// bom & beacon functions
24 13
void bays_init(void);
25

  
26
int assign_bay(int robot);
27
int robot_assignment(int robot);
28
int is_empty(void);
29

  
30
// bom & beacon functions
31 14
void lbom_on(void);
32 15
void lbom_off(void);
33 16
void bay_enable(int which);
branches/autonomous_recharging/code/projects/autonomous_recharging/charging_station/charging.c
1 1
#include "charging.h"
2 2
#include "charging_defs.h"
3
#include "bays.h"
3 4

  
4 5
#include <stdlib.h>
5 6
#include <serial.h>
......
53 54
	for (i = 0; i < NUM_BAYS; i++)
54 55
		bays[i] = -1;
55 56

  
57
	bays_init();
58

  
56 59
	// call our own special function to set the BOM
57 60
	wl_token_ring_set_bom_functions(charging_bom_on,
58 61
		charging_bom_off, charging_bom_read);
......
169 172
		return;
170 173
	}
171 174

  
175
	STATION_DEBUG_PRINT("Cancelling charging for robot ");
176
	STATION_DEBUG_PUTI(robot);
177
	STATION_DEBUG_PRINT(".\n");
178

  
172 179
	//allow other robots to seek
173 180
	if (robots[robot]->state == STATE_SEEKING)
174 181
	{
175
		//TODO : deactivate BOMs and homing beacons
182
		bay_disable();
176 183
		is_seeking = 0;
177 184
	}
178 185

  
......
195 202
{
196 203
	int bay = 0;
197 204

  
198
	//TODO: implement
205
	//TODO: use better algorithm based on distance
206
	for (bay = 0; bay < NUM_BAYS; bay++)
207
		if (bays[bay] == -1)
208
			break;
199 209

  
210
	if (bay == NUM_BAYS)
211
	{
212
		STATION_DEBUG_PRINT("Attempted to assign a bay when all ");
213
		STATION_DEBUG_PRINT("are full.\n");
214
	}
215

  
200 216
	bays[bay] = robot;
201 217
	return bay;
202 218
}
......
208 224
 **/
209 225
void charging_begin_seeking(int robot)
210 226
{
227
	STATION_DEBUG_PRINT("Robot ");
228
	STATION_DEBUG_PUTI(robot);
229
	STATION_DEBUG_PRINT(" has begun seeking.\n");
230
	
211 231
	if (robot > robotsSize)
212 232
	{
213 233
		int nextSize = robot + 1;
......
244 264
				STATION_DEBUG_PRINT("seeking.\n");
245 265
				break;
246 266
			case STATE_DOCKED:
267
				if (is_seeking)
268
				{
269
					//we can only allow one robot to seek
270
					robots[robot]->state = STATE_NOT_CHARGING;
271
					bays[robots[robot]->bay] = -1;
272
					robots[robot]->bay = -1;
273
					break;
274
				}
247 275
				robots[robot]->state = STATE_SEEKING;
248 276
				is_seeking = 1;
249
				//TODO: active robots[robot]->bay
277
				bay_enable(robots[robot]->bay);
250 278
				break;
251 279
			default:
252 280
				STATION_DEBUG_PRINT("Robot is in an ");
......
275 303
	robots[robot] = r;
276 304
	numRobots++;
277 305
	is_seeking = 1;
278
	//TODO: activate r->bay
306
	bay_enable(r->bay);
279 307
}
280 308

  
281 309
/**
......
285 313
 **/
286 314
void charging_dock(int robot)
287 315
{
316
	STATION_DEBUG_PRINT("Robot ");
317
	STATION_DEBUG_PUTI(robot);
318
	STATION_DEBUG_PRINT(" has docked with the station.\n");
319

  
288 320
	if (robots[robot] == NULL)
289 321
	{
290 322
		STATION_DEBUG_PRINT("Should not proceed from not ");
......
300 332

  
301 333
	//robot was seeking before
302 334
	is_seeking = 0;
303
	//TODO: deactivate boms and homing beacons
335
	bay_disable();
304 336
	robots[robot]->state = STATE_DOCKED;
305 337
}
306 338

  
......
351 383
 **/
352 384
void charging_bom_on(void)
353 385
{
354
	//TODO: implement
386
	lbom_on();
355 387
}
356 388

  
357 389
/**
......
360 392
 **/
361 393
void charging_bom_off(void)
362 394
{
363
	//TODO: implement
395
	lbom_off();
364 396
}
365 397

  
366 398
/**
branches/autonomous_recharging/code/projects/autonomous_recharging/charging_station/main.c
16 16
	
17 17
	//dragonfly_init(ALL_ON);
18 18

  
19
	usb_puts("Initializing wireless.\n");
19 20
	wl_init();
20 21
	wl_token_ring_register();
21 22
	charging_init();
22 23

  
24
	usb_puts("Charging station initialized.\n");
25

  
23 26
	while (1)
24 27
		wl_do();
25 28

  
branches/autonomous_recharging/code/projects/autonomous_recharging/dragonfly/wl_recharge_group.c
2 2

  
3 3
#include <wireless.h>
4 4
#include <wl_defs.h>
5
#include <stdio.h>
6
#include <string.h>
7 5

  
6
#include <serial.h>
8 7

  
8
#include "recharge_defs.h"
9

  
9 10
// Function prototypes
10 11
void wl_recharge_timeout_handler(void);
11 12
void wl_recharge_response_handler(int frame, int received);
......
59 60
int requestCount = 0;
60 61
int verifyCount = 0;
61 62

  
63

  
62 64
/**
63 65
 * Register this packet group with the wireless library.
64 66
 * This function must be called before any other wl_recharge
......
111 113
{
112 114
	if (recharge_state != SEEKING)
113 115
	{
114
		WL_DEBUG_PRINT("Unexpected docking occurred.\n");
116
		RECHARGE_DEBUG_PRINT("Unexpected docking occurred.\n");
115 117
		return;
116 118
	}
117 119

  
......
126 128
{
127 129
	if (recharge_state != DOCKED)
128 130
	{
129
		WL_DEBUG_PRINT("Unexpected departure occurred.\n");
131
		RECHARGE_DEBUG_PRINT("Unexpected departure occurred.\n");
130 132
		return;
131 133
	}
132 134
	
......
163 165
void wl_recharge_send_poll(void)
164 166
{
165 167
	wl_send_global_packet(WL_RECHARGE_GROUP, WL_RECHARGE_POLL_STATIONS,
166
				NULL, 0, 0);
168
				0, 0, 0);
167 169
}
168 170

  
169 171
/**
......
174 176
void wl_recharge_send_request(int dest)
175 177
{
176 178
	wl_send_robot_to_robot_global_packet(WL_RECHARGE_GROUP, WL_RECHARGE_REQUEST,
177
				NULL, 0, dest, REQUEST_FRAME);
179
				0, 0, dest, REQUEST_FRAME);
178 180
}
179 181

  
180 182
/**
......
186 188
void wl_recharge_send_verify(int dest)
187 189
{
188 190
	wl_send_robot_to_robot_global_packet(WL_RECHARGE_GROUP, WL_RECHARGE_VERIFY,
189
				NULL, 0, dest, VERIFY_FRAME);
191
				0, 0, dest, VERIFY_FRAME);
190 192
}
191 193

  
192 194
/**
......
196 198
void wl_recharge_send_cancel(int dest)
197 199
{
198 200
	wl_send_robot_to_robot_global_packet(WL_RECHARGE_GROUP, WL_RECHARGE_CANCEL,
199
				NULL, 0, dest, CANCEL_FRAME);
201
				0, 0, dest, CANCEL_FRAME);
200 202
}
201 203

  
202 204
/**
......
208 210
void wl_recharge_send_seeking(int dest)
209 211
{
210 212
	wl_send_robot_to_robot_global_packet(WL_RECHARGE_GROUP, WL_RECHARGE_SEEKING,
211
				NULL, 0, dest, SEEKING_FRAME);
213
				0, 0, dest, SEEKING_FRAME);
212 214
}
213 215

  
214 216
/**
......
220 222
void wl_recharge_send_docked(int dest)
221 223
{
222 224
	wl_send_robot_to_robot_global_packet(WL_RECHARGE_GROUP, WL_RECHARGE_DOCKED,
223
				NULL, 0, dest, DOCKED_FRAME);
225
				0, 0, dest, DOCKED_FRAME);
224 226
}
225 227

  
226 228
/**
......
288 290
		return;
289 291
	if (!received)
290 292
	{
291
		WL_DEBUG_PRINT("Sent packet not recieved.\n");
293
		RECHARGE_DEBUG_PRINT("Sent packet not recieved.\n");
292 294
		switch (frame)
293 295
		{
294 296
			case REQUEST_FRAME:
......
323 325
			wl_recharge_available_station(source);
324 326
			break;
325 327
		case WL_RECHARGE_REQUEST:
326
			WL_DEBUG_PRINT("Only stations should receive WL_RECHARGE_REQUEST.\n");
328
			RECHARGE_DEBUG_PRINT("Only stations should receive WL_RECHARGE_REQUEST.\n");
327 329
			break;
328 330
		case WL_RECHARGE_REQUEST_ACCEPT:
329 331
			wl_recharge_request_accepted(source);
......
341 343
			wl_recharge_docked(source);
342 344
			break;
343 345
		default:
344
			WL_DEBUG_PRINT("Error packet of unknown type received.\n");
346
			RECHARGE_DEBUG_PRINT("Error packet of unknown type received.\n");
345 347
			break;
346 348
	}
347 349
}
......
372 374
{
373 375
	if (recharge_state != REQUESTING)
374 376
	{
375
		WL_DEBUG_PRINT("Accepted when we weren't requesting.\n");
377
		RECHARGE_DEBUG_PRINT("Accepted when we weren't requesting.\n");
376 378
		return;
377 379
	}
378 380
	if (station != source)
379 381
	{
380
		WL_DEBUG_PRINT("Accepted by a different station.\n");
382
		RECHARGE_DEBUG_PRINT("Accepted by a different station.\n");
381 383
		return;
382 384
	}
383 385

  
......
395 397
{
396 398
	if (source != station)
397 399
	{
398
		WL_DEBUG_PRINT("Received verify from unassociated station.\n");
400
		RECHARGE_DEBUG_PRINT("Received verify from unassociated station.\n");
399 401
		wl_recharge_send_cancel(source);
400 402
		return;
401 403
	}
......
409 411
			wl_recharge_send_docked(station);
410 412
			break;
411 413
		default:
412
			WL_DEBUG_PRINT("Cancelled docking procedure.\n");
414
			RECHARGE_DEBUG_PRINT("Cancelled docking procedure.\n");
413 415
			wl_recharge_send_cancel(station);
414 416
			break;
415 417
	}
......
425 427
{
426 428
	if (station != source)
427 429
	{
428
		WL_DEBUG_PRINT("Received cancel from station we weren't docking with.\n");
430
		RECHARGE_DEBUG_PRINT("Received cancel from station we weren't docking with.\n");
429 431
		return;
430 432
	}
431 433

  
......
441 443
			recharge_state = POLLING;
442 444
			break;
443 445
		case DOCKED:
444
			WL_DEBUG_PRINT("Charging station kicked us out.\n");
446
			RECHARGE_DEBUG_PRINT("Charging station kicked us out.\n");
445 447
			wl_recharge_depart();
446 448
			break;
447 449
		case DEPARTING:
448 450
			//ignore, since we are already departing
449 451
			break;
450 452
		default:
451
			WL_DEBUG_PRINT("Received unexpected cancellation.\n");
453
			RECHARGE_DEBUG_PRINT("Received unexpected cancellation.\n");
452 454
			break;
453 455
	}
454 456
}
......
463 465
{
464 466
	if (station != source)
465 467
	{
466
		WL_DEBUG_PRINT("Received seeking alert from station we aren't seeking.\n");
468
		RECHARGE_DEBUG_PRINT("Received seeking alert from station we aren't seeking.\n");
467 469
		wl_recharge_send_cancel(source);
468 470
		return;
469 471
	}
......
478 480
			wl_recharge_send_docked(station);
479 481
			break;
480 482
		default:
481
			WL_DEBUG_PRINT("Received unexpected seeking confirmation.\n");
483
			RECHARGE_DEBUG_PRINT("Received unexpected seeking confirmation.\n");
482 484
			break;
483 485
	}
484 486
}
......
493 495
{
494 496
	if (station != source)
495 497
	{
496
		WL_DEBUG_PRINT("Unknown station believes we are docked.\n");
498
		RECHARGE_DEBUG_PRINT("Unknown station believes we are docked.\n");
497 499
		wl_recharge_send_cancel(source);
498 500
		return;
499 501
	}
......
507 509
		case SEEKING:
508 510
			//not good - we can't immediately proceed to seeking again
509 511
			//we will leave the station and repoll it
510
			WL_DEBUG_PRINT("Seeking, but station believes we are docked. ");
511
			WL_DEBUG_PRINT("Leaving station and repolling.\n");
512
			RECHARGE_DEBUG_PRINT("Seeking, but station believes we are docked. ");
513
			RECHARGE_DEBUG_PRINT("Leaving station and repolling.\n");
512 514
			wl_recharge_send_cancel(source);
513 515
			wl_recharge_begin();
514 516
			break;
515 517
		default:
516
			WL_DEBUG_PRINT("Unexpected docking verification received.\n");
518
			RECHARGE_DEBUG_PRINT("Unexpected docking verification received.\n");
517 519
			break;
518 520
	}
519 521
}
branches/autonomous_recharging/code/projects/autonomous_recharging/dragonfly/recharge.c
1 1
#include <move.h>
2 2
#include <battery.h>
3 3
#include <dio.h>
4
#include <lights.h>
4 5

  
5 6
#include <wl_defs.h>
6 7
#include "wl_recharge_group.h"
7 8
#include "seeking.h"
8 9
#include "departing.h"
10
#include "recharge_i2c.h"
11
#include "recharge_defs.h"
9 12

  
10 13
// Function prototypes
11 14
void recharge_check_low_battery(void);
......
13 16
void recharge_seek(void);
14 17
void recharge_depart(void);
15 18

  
19
//Global variables
20
#ifdef RECHARGE_DEBUG
21
int old_state = -1;
22
#endif
23

  
16 24
/**
17 25
 * Initializes recharging. Before this function may be called,
18 26
 * wl_init must be called.
......
75 83

  
76 84
	// show colors if we are debugging
77 85
	#ifdef RECHARGE_DEBUG
78
	switch (wl_recharge_get_state())
86
	if (old_state != wl_recharge_get_state())
79 87
	{
80
		
88
		old_state = wl_recharge_get_state();
89
		RECHARGE_DEBUG_PUTI(old_state);
90
		switch (wl_recharge_get_state())
91
		{
92
			case NOT_RECHARGING:
93
				orb_set_color(GREEN);
94
				break;
95
			case POLLING:
96
			case REQUESTING:
97
				orb_set_color(RED);
98
				//we shouldn't be in this state
99
				break;
100
			case SEEKING:
101
				orb_set_color(YELLOW);
102
				break;
103
			case DOCKED:
104
				orb_set_color(BLUE);
105
				break;
106
			case DEPARTING:
107
				orb_set_color(PURPLE);
108
				break;
109
			default:
110
				WL_DEBUG_PRINT("Unexpected state.\n");
111
				break;
112
		}
81 113
	}
82 114
	#endif
83 115
	
branches/autonomous_recharging/code/projects/autonomous_recharging/dragonfly/main.c
1 1
#include <dragonfly_lib.h>
2
#include <wireless.h>
3
#include <wl_token_ring.h>
2 4

  
5
#include "recharge.h"
6

  
3 7
int main(void)
4 8
{
5 9
	dragonfly_init(ALL_ON);
10
	usb_puts("Initializing wireless.\n");
11
	wl_init();
12
	wl_token_ring_register();
13
	wl_token_ring_join();
14
	usb_puts("Wireless initialized.\n");
15
	recharge_init();
16
	usb_puts("Recharging initialized.\n");
17

  
18
	while (1)
19
	{
20
		wl_do();
21
		int charging = recharge_do();
22
		if (!charging)
23
		{
24
			//do stuff
25
		}
26
	}
27
	
6 28
	return 0;
7 29
}
8 30

  
branches/autonomous_recharging/code/projects/autonomous_recharging/dragonfly/Makefile
4 4

  
5 5
# Relative path to the root directory (containing lib directory)
6 6
ifndef COLONYROOT
7
COLONYROOT = ../../../..
7
#COLONYROOT = ../../../..
8
COLONYROOT = ../../../../../../trunk
8 9
endif
9 10

  
10 11
# Target file name (without extension).
......
14 15
 USE_WIRELESS = 1
15 16

  
16 17
# com1 = serial port. Use lpt1 to connect to parallel port.
17
AVRDUDE_PORT = com4
18
AVRDUDE_PORT = /dev/ttyUSB0
18 19
#
19 20
#
20 21
###################################

Also available in: Unified diff