CustomMessage Format for ProPTT2

ProPTT2 client is using CustomMessage for special features.(especially sending picture)
ProPTT2 developer can use this format for ProPTT2 client and their clients.
Please refer to https://dev.proptt2.com/sdk-client-ref3-pttchannel.html#sendPublicCustomMessage

1. Format structure

CustomMessage = Custom Message Basic header + { Chunking header } + Custom Message header + Custom Data

2. Custom Message Basic header

FieldTypeDescription
CommandByteRequest command
0x01: SENDPHOTO2CHANNEL
0x02~0x9F: Reserved for ProPTT2
0xA0~0xFF: User command. Other client can use this command.
ContentTypeByte0:Single message, 1:Chunked message
MsgIDShort(2Bytes)You can decide it.

3. Chunking header

FieldTypeDescription
CurrChunkShort(2Bytes)Current index, 1~ChunkCount
ChunkCountShort(2Bytes)Total chunk count.

4. Custom Message header

4.1 SENDPHOTO2CHANNEL(0x01)

FieldTypeDescription
TypeByteImage format
0x01: BMP
0x02: JPEG
0x03: PNG
ReservedByteMust be 0x00
ReservedByteMust be 0x00
ReservedByteMust be 0x00

The custom data must be a image file.
Custom Message Basic header + Chunking header + Custom Message header + Image file data.

< iOS sample for image receiving >


-(void) onPublicCustomMessageRecevied : (PTTChannel*) channelInstance senderID : (NSString*) senderID senderName : (NSString*) senderName dataBuffer : (unsigned char*) dataBuffer dataLength : (int) dataLength
{
    if([[ProvisionTokenRepository getInstance] provChat] == nil || ([[ProvisionTokenRepository getInstance] provChat] != nil && ![[[ProvisionTokenRepository getInstance] provChat] isEqualToString:@"0"]))
	{
		if(_isConnected)
		{
			if(![senderID isEqualToString:_myID])
			{
				if(dataLength >= 16)
				{
					char command = dataBuffer[0];
					switch (command) {
					case 0x01:
						{
							char contentType = dataBuffer[1];
							if((contentType & 0x01) > 0) // chunked
							{
								short msgID = ( dataBuffer[2] << 8 | dataBuffer[3]);
								short currChunk = ( dataBuffer[4] << 8 | dataBuffer[5]);
								short chunkCount = ( dataBuffer[6] << 8 | dataBuffer[7]);
								char type = dataBuffer[8];
								NSString *suffix = @"";
								if(type == 1)
								{
									suffix = @".bmp";
									return; // not support current
								}
								else if(type == 2){
									suffix = @".jpg";
								}
								else if(type == 3){
									suffix = @".png";
								}

								NSString *path = [NSString stringWithFormat:@"%@%@",[_photoFormatter stringFromDate:[NSDate date]],suffix];
								NSData *data = [[NSData alloc] initWithBytes:dataBuffer+12 length:dataLength-12];
								NSString *key = [NSString stringWithFormat:@"%@_%d",senderID,msgID];
								NSData* photo = [self combinePhotoChunkkey photoData:data currChunk:currChunk chunkSize:chunkCount];
								if(photo != nil)
								{
									dispatch_async(dispatch_get_main_queue(), ^{

										if([self isNeedToShowChatNoti])
										{
											[PTTNotification showSentImage:senderName channel:self];
										}

										UIImage *image = [UIImage imageWithData:photo];
										[_repository savePhotoImageSync:path image:image];
										NSArray *records = [self insertChatNewMessage:_client.provisionServerURI account:_myID message:nil channelID:_channel.channelID userID:senderID userName:senderName cellType:ChatCellTypeReceiverPhoto path:path fileSize:[photo length] imageState:ImageStateCompleted width:image.size.width height:image.size.height location:@""];

										for(NSManagedObject *record in records)
										{
											id channelDelegate = [self getVisibleDelegate];
											if([channelDelegate respondsToSelector:@selector(onPublicCustomMessageRecevied:senderID:senderName:dataBuffer:dataLength:)])
											{
												[channelDelegate onPublicCustomMessageRecevied:self senderID:senderID senderName:senderName dataBuffer:dataBuffer dataLength:dataLength];
											}
											if([channelDelegate respondsToSelector:@selector(onPublicCustomMessageRecevied:managedObject:)])
											{
												[channelDelegate onPublicCustomMessageRecevied:self managedObject:record];
											}
											if([_channelDelegate respondsToSelector:@selector(onPublicCustomMessageRecevied:senderID:senderName:dataBuffer:dataLength:)])
											{
												[_channelDelegate onPublicCustomMessageRecevied:self senderID:senderID senderName:senderName dataBuffer:dataBuffer dataLength:dataLength];
											}
											if([_channelDelegate respondsToSelector:@selector(onPublicCustomMessageRecevied:managedObject:)])
											{
												[_channelDelegate onPublicCustomMessageRecevied:self managedObject:record];
											}
										}
									});
								}
							}
						}
						break;
					default:
						break;
					}
				}
			}
		}
    }
}

-(NSData*) combinePhotoChunk(NSString*) msgID photoData : (NSData*) data currChunk : (short) currChunk chunkSize : (short) chunkSize
{
    NSMutableDictionary *photoData = [_photoMap objectForKey:msgID];
    if(photoData == nil)
    {
        photoData = [[NSMutableDictionary alloc] init];
        [_photoMap setObject:photoData forKey:msgID];
    }

    [photoData setObject:data forKey:@(currChunk)];
    if([photoData count] == chunkSize)
    {
        NSMutableData *result = [[NSMutableData alloc] init];
        for(NSInteger i=0; i < chunkSize; ++i)
        {
            NSData *segment = [photoData objectForKey:@(i+1)];
            [result appendData:segment];
        }

        [_photoMap removeObjectForKey:msgID];
        return result;
    }

    return nil;
}