xref: /openbmc/ipmitool/src/plugins/usb/usb.c (revision e2c5b322)
1 /*
2  * Copyright (c) 2015 American Megatrends, Inc.
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  *
9  * 1. Redistributions of source code must retain the above copyright notice,
10  *    this list of conditions and the following disclaimer.
11  *
12  * 2. Redistributions in binary form must reproduce the above copyright notice,
13  *    this list of conditions and the following disclaimer in the documentation
14  *    and/or other materials provided with the distribution.
15  *
16  * 3. Neither the name of the copyright holder nor the names of its
17  *    contributors may be used to endorse or promote products derived from this
18  *    software without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23  * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30  * POSSIBILITY OF SUCH DAMAGE.
31  */
32 
33 #define _BSD_SOURCE
34 
35 #include <ipmitool/helper.h>
36 #include <ipmitool/log.h>
37 #include <ipmitool/bswap.h>
38 #include <ipmitool/ipmi.h>
39 #include <ipmitool/ipmi_intf.h>
40 #include <ipmitool/ipmi_oem.h>
41 #include <ipmitool/ipmi_strings.h>
42 #include <ipmitool/ipmi_constants.h>
43 #include <scsi/sg.h>
44 #include <sys/ioctl.h>
45 #include <scsi/scsi_ioctl.h>
46 #include <scsi/scsi.h>
47 #include <sys/file.h>
48 #include <sys/stat.h>
49 #include <sys/types.h>
50 #include <fcntl.h>
51 #include <errno.h>
52 #include <unistd.h>
53 
54 #define PACKED __attribute__ ((packed))
55 #define BEGIN_SIG                   "$G2-CONFIG-HOST$"
56 #define BEGIN_SIG_LEN               16
57 #define MAX_REQUEST_SIZE            64 * 1024
58 #define CMD_RESERVED                0x0000
59 #define SCSI_AMICMD_CURI_WRITE      0xE2
60 #define SCSI_AMICMD_CURI_READ       0xE3
61 #define SCSI_AMIDEF_CMD_SECTOR      0x01
62 #define SCSI_AMIDEF_DATA_SECTOR     0x02
63 #define ERR_SUCCESS                 0       /* Success */
64 #define ERR_BIG_DATA                1       /* Too Much Data */
65 #define ERR_NO_DATA                 2       /* No/Less Data Available */
66 #define ERR_UNSUPPORTED             3       /* Unsupported Command */
67 #define IN_PROCESS                  0x8000  /* Bit 15 of Status */
68 #define SCSI_AMICMD_ID              0xEE
69 
70 /* SCSI Command Packets */
71 typedef struct {
72 	unsigned char   OpCode;
73 	unsigned char   Lun;
74 	unsigned int    Lba;
75 	union {
76 		struct {
77 			unsigned char   Reserved6;
78 			unsigned short  Length;
79 			unsigned char   Reserved9[3];
80 		} PACKED Cmd10;
81 		struct Len32 {
82 			unsigned int    Length32;
83 			unsigned char   Reserved10[2];
84 		} PACKED Cmd12;
85 	} PACKED CmdLen;
86 } PACKED SCSI_COMMAND_PACKET;
87 
88 typedef struct {
89 	uint8_t byNetFnLUN;
90 	uint8_t byCmd;
91 	uint8_t byData[MAX_REQUEST_SIZE];
92 } PACKED IPMIUSBRequest_T;
93 
94 typedef struct {
95 	uint8_t   BeginSig[BEGIN_SIG_LEN];
96 	uint16_t  Command;
97 	uint16_t  Status;
98 	uint32_t  DataInLen;
99 	uint32_t  DataOutLen;
100 	uint32_t  InternalUseDataIn;
101 	uint32_t  InternalUseDataOut;
102 } CONFIG_CMD;
103 
104 static int ipmi_usb_setup(struct ipmi_intf *intf);
105 static struct ipmi_rs *ipmi_usb_send_cmd(struct ipmi_intf *intf,
106 		struct ipmi_rq *req);
107 
108 struct ipmi_intf ipmi_usb_intf = {
109 	.name = "usb",
110 	.desc = "IPMI USB Interface(OEM Interface for AMI Devices)",
111 	.setup = ipmi_usb_setup,
112 	.sendrecv = ipmi_usb_send_cmd,
113 };
114 
115 int
scsiProbeNew(int * num_ami_devices,int * sg_nos)116 scsiProbeNew(int *num_ami_devices, int *sg_nos)
117 {
118 	int inplen = *num_ami_devices;
119 	int numdevfound = 0;
120 	char linebuf[81];
121 	char vendor[81];
122 	int lineno = 0;
123 	FILE *fp;
124 
125 	fp = fopen("/proc/scsi/sg/device_strs", "r");
126 	if (fp == NULL) {
127 		/* Return 1 on error */
128 		return 1;
129 	}
130 
131 	while (1) {
132 		/* Read line by line and search for "AMI" */
133 		if (fgets(linebuf, 80, fp) == NULL) {
134 			if (fp != NULL) {
135 				fclose(fp);
136 			}
137 			/* Return 1 on error */
138 			return 1;
139 		}
140 
141 		if (sscanf(linebuf, "%s", vendor) == 1) {
142 			if (strncmp(vendor, "AMI", strlen("AMI")) == 0) {
143 				numdevfound++;
144 				sg_nos[numdevfound - 1] = lineno;
145 				if (numdevfound == inplen) {
146 					break;
147 				}
148 			}
149 			lineno++;
150 		}
151 	}
152 
153 	*num_ami_devices = numdevfound;
154 	if (fp != NULL) {
155 		fclose(fp);
156 	}
157 
158 	return 0;
159 }
160 
161 int
OpenCD(struct ipmi_intf * intf,char * CDName)162 OpenCD(struct ipmi_intf *intf, char *CDName)
163 {
164 	intf->fd = open(CDName, O_RDWR);
165 	if (intf->fd == (-1)) {
166 		lprintf(LOG_ERR, "OpenCD:Unable to open device, %s",
167 				strerror(errno));
168 		return 1;
169 	}
170 	return 0;
171 }
172 
173 int
sendscsicmd_SGIO(int cd_desc,unsigned char * cdb_buf,unsigned char cdb_len,void * data_buf,unsigned int * data_len,int direction,void * sense_buf,unsigned char slen,unsigned int timeout)174 sendscsicmd_SGIO(int cd_desc, unsigned char *cdb_buf, unsigned char cdb_len,
175 		void *data_buf, unsigned int *data_len, int direction,
176 		void *sense_buf, unsigned char slen, unsigned int timeout)
177 {
178 	sg_io_hdr_t io_hdr;
179 
180 	/* Prepare command */
181 	memset(&io_hdr, 0, sizeof(sg_io_hdr_t));
182 	io_hdr.interface_id = 'S';
183 	io_hdr.cmd_len = cdb_len;
184 
185 	/* Transfer direction and length */
186 	io_hdr.dxfer_direction = direction;
187 	io_hdr.dxfer_len = *data_len;
188 
189 	io_hdr.dxferp = data_buf;
190 
191 	io_hdr.cmdp = cdb_buf;
192 
193 	io_hdr.sbp = (unsigned char *)sense_buf;
194 	io_hdr.mx_sb_len = slen;
195 
196 	io_hdr.timeout = timeout;
197 
198 	if (!timeout) {
199 		io_hdr.timeout = 20000;
200 	}
201 
202 	if (ioctl(cd_desc, SG_IO, &io_hdr) < 0) {
203 		lprintf(LOG_ERR, "sendscsicmd_SGIO: SG_IO ioctl error");
204 		return 1;
205 	} else {
206 		if (io_hdr.status != 0) {
207 			return 1;
208 		}
209 	}
210 
211 	if (!timeout) {
212 		return 0;
213 	}
214 
215 	if ((io_hdr.info & SG_INFO_OK_MASK) != SG_INFO_OK) {
216 		lprintf(LOG_DEBUG, "sendscsicmd_SGIO: SG_INFO_OK - Not OK");
217 	} else {
218 		lprintf(LOG_DEBUG, "sendscsicmd_SGIO: SG_INFO_OK - OK");
219 		return 0;
220 	}
221 
222 	return 1;
223 }
224 
225 int
AMI_SPT_CMD_Identify(int cd_desc,char * szSignature)226 AMI_SPT_CMD_Identify(int cd_desc, char *szSignature)
227 {
228 	SCSI_COMMAND_PACKET IdPkt = {0};
229 	int ret;
230 	unsigned int siglen = 10;
231 
232 	IdPkt.OpCode = SCSI_AMICMD_ID;
233 	ret = sendscsicmd_SGIO(cd_desc, (unsigned char *)&IdPkt,
234 				10, szSignature, &siglen, SG_DXFER_FROM_DEV,
235 				NULL, 0, 5000);
236 
237 	return ret;
238 }
239 
240 int
IsG2Drive(int cd_desc)241 IsG2Drive(int cd_desc)
242 {
243 	char szSignature[15];
244 	int ret;
245 
246 	memset(szSignature, 0, 15);
247 
248 	flock(cd_desc, LOCK_EX);
249 	ret = AMI_SPT_CMD_Identify(cd_desc, szSignature);
250 	flock(cd_desc, LOCK_UN);
251 	if (ret != 0) {
252 		lprintf(LOG_DEBUG,
253 				"IsG2Drive:Unable to send ID command to the device");
254 		return 1;
255 	}
256 
257 	if (strncmp(szSignature, "$$$AMI$$$", strlen("$$$AMI$$$")) != 0) {
258 		lprintf(LOG_ERR,
259 				"IsG2Drive:Signature mismatch when ID command sent");
260 		return 1;
261 	}
262 
263 	return 0;
264 }
265 
266 int
FindG2CDROM(struct ipmi_intf * intf)267 FindG2CDROM(struct ipmi_intf *intf)
268 {
269 	int err = 0;
270 	char device[256];
271 	int devarray[8];
272 	int numdev = 8;
273 	int iter;
274 	err = scsiProbeNew(&numdev, devarray);
275 
276 	if (err == 0 && numdev > 0) {
277 		for (iter = 0; iter < numdev; iter++) {
278 			sprintf(device, "/dev/sg%d", devarray[iter]);
279 
280 			if (!OpenCD(intf, device)) {
281 				if (!IsG2Drive(intf->fd)) {
282 					lprintf(LOG_DEBUG, "USB Device found");
283 					return 1;
284 				}
285 				close(intf->fd);
286 			}
287 		}
288 	} else {
289 		lprintf(LOG_DEBUG, "Unable to find Virtual CDROM Device");
290 	}
291 
292 	return 0;
293 }
294 
295 static int
ipmi_usb_setup(struct ipmi_intf * intf)296 ipmi_usb_setup(struct ipmi_intf *intf)
297 {
298 	if (FindG2CDROM(intf) == 0) {
299 		lprintf(LOG_ERR, "Error in USB session setup \n");
300 		return (-1);
301 	}
302 	intf->opened = 1;
303 	return 0;
304 }
305 
306 void
InitCmdHeader(CONFIG_CMD * pG2CDCmdHeader)307 InitCmdHeader(CONFIG_CMD *pG2CDCmdHeader)
308 {
309 	memset(pG2CDCmdHeader, 0, sizeof(CONFIG_CMD));
310 	memcpy((char *)pG2CDCmdHeader->BeginSig, BEGIN_SIG, BEGIN_SIG_LEN);
311 }
312 
313 int
AMI_SPT_CMD_SendCmd(int cd_desc,char * Buffer,char type,uint16_t buflen,unsigned int timeout)314 AMI_SPT_CMD_SendCmd(int cd_desc, char *Buffer, char type, uint16_t buflen,
315 		unsigned int timeout)
316 {
317 	SCSI_COMMAND_PACKET Cmdpkt;
318 	char sensebuff[32];
319 	int ret;
320 	unsigned int pktLen;
321 	int count = 3;
322 
323 	memset(&Cmdpkt, 0, sizeof(SCSI_COMMAND_PACKET));
324 
325 	Cmdpkt.OpCode = SCSI_AMICMD_CURI_WRITE;
326 	Cmdpkt.Lba = htonl(type);
327 	Cmdpkt.CmdLen.Cmd10.Length = htons(1);
328 
329 	pktLen = buflen;
330 	while (count > 0) {
331 		ret = sendscsicmd_SGIO(cd_desc, (unsigned char *)&Cmdpkt,
332 				10, Buffer, &pktLen, SG_DXFER_TO_DEV,
333 				sensebuff, 32, timeout);
334 		count--;
335 		if (ret == 0) {
336 			break;
337 		} else {
338 			ret = (-1);
339 		}
340 	}
341 
342 	return ret;
343 }
344 
345 int
AMI_SPT_CMD_RecvCmd(int cd_desc,char * Buffer,char type,uint16_t buflen)346 AMI_SPT_CMD_RecvCmd(int cd_desc, char *Buffer, char type, uint16_t buflen)
347 {
348 	SCSI_COMMAND_PACKET Cmdpkt;
349 	char sensebuff[32];
350 	int ret;
351 	unsigned int pktLen;
352 	int count = 3;
353 
354 	memset(&Cmdpkt, 0, sizeof(SCSI_COMMAND_PACKET));
355 
356 	Cmdpkt.OpCode = SCSI_AMICMD_CURI_READ;
357 	Cmdpkt.Lba = htonl(type);
358 	Cmdpkt.CmdLen.Cmd10.Length = htons(1);
359 
360 	pktLen = buflen;
361 	while (count > 0) {
362 		ret = sendscsicmd_SGIO(cd_desc, (unsigned char *)&Cmdpkt,
363 				10, Buffer, &pktLen, SG_DXFER_FROM_DEV,
364 				sensebuff, 32, 5000);
365 		count--;
366 		if (0 == ret) {
367 			break;
368 		} else {
369 			ret = (-1);
370 		}
371 	}
372 
373 	return ret;
374 }
375 
376 int
ReadCD(int cd_desc,char CmdData,char * Buffer,uint32_t DataLen)377 ReadCD(int cd_desc, char CmdData, char *Buffer, uint32_t DataLen)
378 {
379 	int ret;
380 
381 	ret = AMI_SPT_CMD_RecvCmd(cd_desc, Buffer, CmdData, DataLen);
382 	if (ret != 0) {
383 		lprintf(LOG_ERR, "Error while reading CD-Drive");
384 		return (-1);
385 	}
386 	return 0;
387 }
388 
389 int
WriteCD(int cd_desc,char CmdData,char * Buffer,unsigned int timeout,uint32_t DataLen)390 WriteCD(int cd_desc, char CmdData, char *Buffer, unsigned int timeout,
391 		uint32_t DataLen)
392 {
393 	int ret;
394 
395 	ret = AMI_SPT_CMD_SendCmd(cd_desc, Buffer, CmdData, DataLen, timeout);
396 	if (ret != 0) {
397 		lprintf(LOG_ERR, "Error while writing to CD-Drive");
398 		return (-1);
399 	}
400 	return 0;
401 }
402 
403 int
WriteSplitData(struct ipmi_intf * intf,char * Buffer,char Sector,uint32_t NumBytes,uint32_t timeout)404 WriteSplitData(struct ipmi_intf *intf, char *Buffer, char Sector,
405 			uint32_t NumBytes, uint32_t timeout)
406 {
407 	uint32_t BytesWritten = 0;
408 	int retVal;
409 
410 	if (NumBytes == 0) {
411 		return 0;
412 	}
413 
414 	while (BytesWritten < NumBytes) {
415 		if ((retVal = WriteCD(intf->fd, Sector,
416 						(Buffer + BytesWritten),
417 						timeout, NumBytes)) != 0) {
418 			return retVal;
419 		}
420 
421 		BytesWritten += NumBytes;
422 	}
423 
424 	return 0;
425 }
426 
427 int
ReadSplitData(struct ipmi_intf * intf,char * Buffer,char Sector,uint32_t NumBytes)428 ReadSplitData(struct ipmi_intf *intf, char *Buffer, char Sector,
429 				uint32_t NumBytes)
430 {
431 	uint32_t BytesRead = 0;
432 
433 	if (NumBytes == 0) {
434 		return 0;
435 	}
436 
437 	while (BytesRead < NumBytes) {
438 		if (ReadCD(intf->fd, Sector, (Buffer + BytesRead),
439 					NumBytes) == (-1)) {
440 			return 1;
441 		}
442 		BytesRead += NumBytes;
443 	}
444 
445 	return 0;
446 }
447 
448 int
WaitForCommandCompletion(struct ipmi_intf * intf,CONFIG_CMD * pG2CDCmdHeader,uint32_t timeout,uint32_t DataLen)449 WaitForCommandCompletion(struct ipmi_intf *intf, CONFIG_CMD *pG2CDCmdHeader,
450 		uint32_t timeout, uint32_t DataLen)
451 {
452 	uint32_t TimeCounter = 0;
453 
454 	do {
455 		if (ReadCD(intf->fd, SCSI_AMIDEF_CMD_SECTOR,
456 					(char *)(pG2CDCmdHeader), DataLen) == (-1)) {
457 			lprintf(LOG_ERR, "ReadCD returned ERROR");
458 			return 1;
459 		}
460 
461 		if (pG2CDCmdHeader->Status & IN_PROCESS) {
462 			usleep(1000);
463 			if (timeout > 0) {
464 				TimeCounter++;
465 				if (TimeCounter == (timeout + 1)) {
466 					return 2;
467 				}
468 			}
469 		} else {
470 			lprintf(LOG_DEBUG, "Command completed");
471 			break;
472 		}
473 	} while (1);
474 
475 	return 0;
476 }
477 
478 int
SendDataToUSBDriver(struct ipmi_intf * intf,char * ReqBuffer,unsigned int ReqBuffLen,unsigned char * ResBuffer,int * ResBuffLen,unsigned int timeout)479 SendDataToUSBDriver(struct ipmi_intf *intf, char *ReqBuffer,
480 			unsigned int ReqBuffLen, unsigned char *ResBuffer,
481 			int *ResBuffLen, unsigned int timeout)
482 {
483 	char CmdHeaderBuffer[sizeof(CONFIG_CMD)];
484 	int retVal;
485 	int waitretval = 0;
486 	unsigned int to = 0;
487 	uint32_t DataLen = 0;
488 
489 	CONFIG_CMD *pG2CDCmdHeader = (CONFIG_CMD *)CmdHeaderBuffer;
490 
491 	/* FillHeader */
492 	InitCmdHeader(pG2CDCmdHeader);
493 
494 	/* Set command number */
495 	pG2CDCmdHeader->Command = CMD_RESERVED;
496 
497 	/* Fill Lengths */
498 	pG2CDCmdHeader->DataOutLen = *ResBuffLen;
499 	pG2CDCmdHeader->DataInLen = ReqBuffLen;
500 
501 	if (!timeout) {
502 		to = 3000;
503 	}
504 
505 	DataLen = sizeof(CONFIG_CMD);
506 
507 	if (WriteCD(intf->fd, SCSI_AMIDEF_CMD_SECTOR,
508 				(char *)(pG2CDCmdHeader), to, DataLen) == (-1)) {
509 		lprintf(LOG_ERR,
510 				"Error in Write CD of SCSI_AMIDEF_CMD_SECTOR");
511 		return (-1);
512 	}
513 
514 	/* Write the data to hard disk */
515 	if ((retVal = WriteSplitData(intf, ReqBuffer,
516 					SCSI_AMIDEF_DATA_SECTOR,
517 					ReqBuffLen, timeout)) != 0) {
518 		lprintf(LOG_ERR,
519 				"Error in WriteSplitData of SCSI_AMIDEF_DATA_SECTOR");
520 		return (-1);
521 	}
522 
523 	if (!timeout) {
524 		return 0;
525 	}
526 
527 	/* Read Status now */
528 	waitretval = WaitForCommandCompletion(intf, pG2CDCmdHeader, timeout,
529 			DataLen);
530 	if (waitretval != 0) {
531 		lprintf(LOG_ERR, "WaitForCommandComplete failed");
532 		return (0 - waitretval);
533 	} else {
534 		lprintf(LOG_DEBUG, "WaitForCommandCompletion SUCCESS");
535 	}
536 
537 	switch (pG2CDCmdHeader->Status) {
538 		case ERR_SUCCESS:
539 			*ResBuffLen = pG2CDCmdHeader->DataOutLen;
540 			lprintf(LOG_DEBUG, "Before ReadSplitData %x", *ResBuffLen);
541 			if (ReadSplitData(intf, (char *)ResBuffer,
542 						SCSI_AMIDEF_DATA_SECTOR,
543 						pG2CDCmdHeader->DataOutLen) != 0) {
544 				lprintf(LOG_ERR,
545 						"Err ReadSplitData SCSI_AMIDEF_DATA_SCTR");
546 				return (-1);
547 			}
548 			/* Additional read to see verify there was not problem
549 			 * with the previous read
550 			 */
551 			DataLen = sizeof(CONFIG_CMD);
552 			ReadCD(intf->fd, SCSI_AMIDEF_CMD_SECTOR,
553 					(char *)(pG2CDCmdHeader), DataLen);
554 			break;
555 		case ERR_BIG_DATA:
556 			lprintf(LOG_ERR, "Too much data");
557 			break;
558 		case ERR_NO_DATA:
559 			lprintf(LOG_ERR, "Too little data");
560 			break;
561 		case ERR_UNSUPPORTED:
562 			lprintf(LOG_ERR, "Unsupported command");
563 			break;
564 		default:
565 			lprintf(LOG_ERR, "Unknown status");
566 	}
567 
568 	return pG2CDCmdHeader->Status;
569 }
570 
571 static struct ipmi_rs *
ipmi_usb_send_cmd(struct ipmi_intf * intf,struct ipmi_rq * req)572 ipmi_usb_send_cmd(struct ipmi_intf *intf, struct ipmi_rq *req)
573 {
574 	static struct ipmi_rs rsp;
575 	long timeout = 20000;
576 	uint8_t byRet = 0;
577 	char ReqBuff[MAX_REQUEST_SIZE] = {0};
578 	IPMIUSBRequest_T *pReqPkt = (IPMIUSBRequest_T *)ReqBuff;
579 	int retries = 0;
580 	/********** FORM IPMI PACKET *****************/
581 	pReqPkt->byNetFnLUN = req->msg.netfn << 2;
582 	pReqPkt->byNetFnLUN += req->msg.lun;
583 	pReqPkt->byCmd = req->msg.cmd;
584 	if (req->msg.data_len) {
585 		memcpy(pReqPkt->byData, req->msg.data, req->msg.data_len);
586 	}
587 
588 	/********** SEND DATA TO USB ******************/
589 	while (retries < 3) {
590 		retries++;
591 		byRet = SendDataToUSBDriver(intf, ReqBuff,
592 				2 + req->msg.data_len, rsp.data,
593 				&rsp.data_len,timeout);
594 
595 		if (byRet == 0) {
596 			break;
597 		}
598 	}
599 
600 	if (retries == 3) {
601 		lprintf(LOG_ERR,
602 				"Error while sending command using",
603 				"SendDataToUSBDriver");
604 		rsp.ccode = byRet;
605 		return &rsp;
606 	}
607 
608 	rsp.ccode = rsp.data[0];
609 
610 	/* Save response data for caller */
611 	if ((rsp.ccode == 0) && (rsp.data_len > 0)) {
612 		memmove(rsp.data, rsp.data + 1, rsp.data_len - 1);
613 		rsp.data[rsp.data_len] = 0;
614 		rsp.data_len -= 1;
615 	}
616 	return &rsp;
617 }
618