Verify configuration before writing it into the radio.

This commit is contained in:
Serge Vakulenko 2018-08-31 18:19:53 -07:00
parent 5a842d8770
commit a9a6d691b1
6 changed files with 499 additions and 83 deletions

5
dfu.c
View File

@ -284,6 +284,9 @@ static void erase_block(uint32_t address)
} }
get_status(); get_status();
wait_dfu_idle(); wait_dfu_idle();
fprintf(stderr, "#");
fflush(stderr);
} }
static const char *identify() static const char *identify()
@ -369,6 +372,8 @@ void dfu_close()
void dfu_erase(int nbytes) void dfu_erase(int nbytes)
{ {
// Enter Programming Mode. // Enter Programming Mode.
get_status();
wait_dfu_idle();
md380_command(0x91, 0x01); md380_command(0x91, 0x01);
usleep(100000); usleep(100000);

2
main.c
View File

@ -120,6 +120,7 @@ int main(int argc, char **argv)
radio_read_image(argv[0]); radio_read_image(argv[0]);
radio_print_version(stdout); radio_print_version(stdout);
radio_parse_config(argv[1]); radio_parse_config(argv[1]);
radio_verify_config();
radio_save_image("device.img"); radio_save_image("device.img");
} else { } else {
@ -129,6 +130,7 @@ int main(int argc, char **argv)
radio_print_version(stdout); radio_print_version(stdout);
radio_save_image("backup.img"); radio_save_image("backup.img");
radio_parse_config(argv[0]); radio_parse_config(argv[0]);
radio_verify_config();
radio_upload(1); radio_upload(1);
radio_disconnect(); radio_disconnect();
} }

260
md380.c
View File

@ -51,6 +51,16 @@
#define OFFSET_SCANL 0x18860 #define OFFSET_SCANL 0x18860
#define OFFSET_CHANNELS 0x1ee00 #define OFFSET_CHANNELS 0x1ee00
#define GET_TIMESTAMP() (&radio_mem[OFFSET_TIMESTMP])
#define GET_SETTINGS() ((general_settings_t*) &radio_mem[OFFSET_SETTINGS])
#define GET_CHANNEL(i) ((channel_t*) &radio_mem[OFFSET_CHANNELS + (i)*64])
#define GET_ZONE(i) ((zone_t*) &radio_mem[OFFSET_ZONES + (i)*64])
#define GET_ZONEXT(i) ((zone_ext_t*) &radio_mem[OFFSET_ZONEXT + (i)*224])
#define GET_SCANLIST(i) ((scanlist_t*) &radio_mem[OFFSET_SCANL + (i)*104])
#define GET_CONTACT(i) ((contact_t*) &radio_mem[OFFSET_CONTACTS + (i)*36])
#define GET_GROUPLIST(i) ((grouplist_t*) &radio_mem[OFFSET_GLISTS + (i)*96])
#define GET_MESSAGE(i) ((uint16_t*) &radio_mem[OFFSET_MSG + (i)*288])
// //
// Channel data. // Channel data.
// //
@ -325,7 +335,7 @@ static const char *SIGNALING_SYSTEM[] = { "-", "DTMF-1", "DTMF-2", "DTMF-3", "DT
// //
static void md380_print_version(radio_device_t *radio, FILE *out) static void md380_print_version(radio_device_t *radio, FILE *out)
{ {
unsigned char *timestamp = &radio_mem[OFFSET_TIMESTMP]; unsigned char *timestamp = GET_TIMESTAMP();
static const char charmap[16] = "0123456789:;<=>?"; static const char charmap[16] = "0123456789:;<=>?";
if (*timestamp != 0xff) { if (*timestamp != 0xff) {
@ -368,8 +378,6 @@ static void md380_upload(radio_device_t *radio, int cont_flag)
dfu_erase(MEMSZ); dfu_erase(MEMSZ);
fprintf(stderr, "Sending data... ");
fflush(stderr);
for (bno=0; bno<MEMSZ/1024; bno++) { for (bno=0; bno<MEMSZ/1024; bno++) {
dfu_write_block(bno, &radio_mem[bno*1024], 1024); dfu_write_block(bno, &radio_mem[bno*1024], 1024);
@ -394,7 +402,7 @@ static int md380_is_compatible(radio_device_t *radio)
// //
static void setup_zone(int index, const char *name) static void setup_zone(int index, const char *name)
{ {
zone_t *z = (zone_t*) &radio_mem[OFFSET_ZONES + index*64]; zone_t *z = GET_ZONE(index);
utf8_decode(z->name, name, 16); utf8_decode(z->name, name, 16);
} }
@ -403,9 +411,9 @@ static void setup_zone(int index, const char *name)
// Add channel to a zone. // Add channel to a zone.
// Return 0 on failure. // Return 0 on failure.
// //
static int zone_append(int zone_index, int cnum) static int zone_append(int index, int cnum)
{ {
zone_t *z = (zone_t*) &radio_mem[OFFSET_ZONES + zone_index*64]; zone_t *z = GET_ZONE(index);
int i; int i;
for (i=0; i<16; i++) { for (i=0; i<16; i++) {
@ -419,9 +427,9 @@ static int zone_append(int zone_index, int cnum)
return 0; return 0;
} }
static void erase_zone(int zone_index) static void erase_zone(int index)
{ {
zone_t *z = (zone_t*) &radio_mem[OFFSET_ZONES + zone_index*64]; zone_t *z = GET_ZONE(index);
memset(z, 0, 64); memset(z, 0, 64);
} }
@ -432,7 +440,7 @@ static void erase_zone(int zone_index)
static void setup_scanlist(int index, const char *name, static void setup_scanlist(int index, const char *name,
int prio1, int prio2, int txchan) int prio1, int prio2, int txchan)
{ {
scanlist_t *sl = (scanlist_t*) &radio_mem[OFFSET_SCANL + index*104]; scanlist_t *sl = GET_SCANLIST(index);
// Bytes 0-31 // Bytes 0-31
utf8_decode(sl->name, name, 16); utf8_decode(sl->name, name, 16);
@ -445,7 +453,7 @@ static void setup_scanlist(int index, const char *name,
static void erase_scanlist(int index) static void erase_scanlist(int index)
{ {
scanlist_t *sl = (scanlist_t*) &radio_mem[OFFSET_SCANL + index*104]; scanlist_t *sl = GET_SCANLIST(index);
memset(sl, 0, 104); memset(sl, 0, 104);
@ -465,9 +473,9 @@ static void erase_scanlist(int index)
// Add channel to a zone. // Add channel to a zone.
// Return 0 on failure. // Return 0 on failure.
// //
static int scanlist_append(int list_index, int cnum) static int scanlist_append(int index, int cnum)
{ {
scanlist_t *sl = (scanlist_t*) &radio_mem[OFFSET_SCANL + list_index*104]; scanlist_t *sl = GET_SCANLIST(index);
int i; int i;
for (i=0; i<31; i++) { for (i=0; i<31; i++) {
@ -483,7 +491,7 @@ static int scanlist_append(int list_index, int cnum)
static void erase_contact(int index) static void erase_contact(int index)
{ {
contact_t *ct = (contact_t*) &radio_mem[OFFSET_CONTACTS + index*36]; contact_t *ct = GET_CONTACT(index);
memset(ct, 0, 36); memset(ct, 0, 36);
*(uint32_t*)ct = 0xffffffff; *(uint32_t*)ct = 0xffffffff;
@ -491,7 +499,7 @@ static void erase_contact(int index)
static void setup_contact(int index, const char *name, int type, int id, int rxtone) static void setup_contact(int index, const char *name, int type, int id, int rxtone)
{ {
contact_t *ct = (contact_t*) &radio_mem[OFFSET_CONTACTS + index*36]; contact_t *ct = GET_CONTACT(index);
ct->id = id; ct->id = id;
ct->type = type; ct->type = type;
@ -502,7 +510,7 @@ static void setup_contact(int index, const char *name, int type, int id, int rxt
static void setup_grouplist(int index, const char *name) static void setup_grouplist(int index, const char *name)
{ {
grouplist_t *gl = (grouplist_t*) &radio_mem[OFFSET_GLISTS + index*96]; grouplist_t *gl = GET_GROUPLIST(index);
utf8_decode(gl->name, name, 16); utf8_decode(gl->name, name, 16);
} }
@ -513,7 +521,7 @@ static void setup_grouplist(int index, const char *name)
// //
static int grouplist_append(int index, int cnum) static int grouplist_append(int index, int cnum)
{ {
grouplist_t *gl = (grouplist_t*) &radio_mem[OFFSET_GLISTS + index*96]; grouplist_t *gl = GET_GROUPLIST(index);
int i; int i;
for (i=0; i<32; i++) { for (i=0; i<32; i++) {
@ -532,7 +540,7 @@ static int grouplist_append(int index, int cnum)
// //
static void setup_message(int index, const char *text) static void setup_message(int index, const char *text)
{ {
uint16_t *msg = (uint16_t*) &radio_mem[OFFSET_MSG + index*288]; uint16_t *msg = GET_MESSAGE(index);
// Skip spaces and tabs. // Skip spaces and tabs.
while (*text == ' ' || *text == '\t') while (*text == ' ' || *text == '\t')
@ -560,7 +568,7 @@ static void setup_channel(int i, int mode, char *name, double rx_mhz, double tx_
int admit, int colorcode, int timeslot, int incall, int grouplist, int contact, int admit, int colorcode, int timeslot, int incall, int grouplist, int contact,
int rxtone, int txtone, int width) int rxtone, int txtone, int width)
{ {
channel_t *ch = (channel_t*) &radio_mem[OFFSET_CHANNELS + i*64]; channel_t *ch = GET_CHANNEL(i);
ch->channel_mode = mode; ch->channel_mode = mode;
ch->bandwidth = width; ch->bandwidth = width;
@ -590,7 +598,7 @@ static void setup_channel(int i, int mode, char *name, double rx_mhz, double tx_
// //
static void erase_channel(int i) static void erase_channel(int i)
{ {
channel_t *ch = (channel_t*) &radio_mem[OFFSET_CHANNELS + i*64]; channel_t *ch = GET_CHANNEL(i);
// Byte 0 // Byte 0
ch->channel_mode = MODE_ANALOG; ch->channel_mode = MODE_ANALOG;
@ -706,7 +714,7 @@ static void print_chanlist(FILE *out, uint16_t *unsorted, int nchan)
static void print_id(FILE *out, int verbose) static void print_id(FILE *out, int verbose)
{ {
general_settings_t *gs = (general_settings_t*) &radio_mem[OFFSET_SETTINGS]; general_settings_t *gs = GET_SETTINGS();
unsigned id = gs->radio_id[0] | (gs->radio_id[1] << 8) | (gs->radio_id[2] << 16); unsigned id = gs->radio_id[0] | (gs->radio_id[1] << 8) | (gs->radio_id[2] << 16);
if (verbose) if (verbose)
@ -722,7 +730,7 @@ static void print_id(FILE *out, int verbose)
static void print_intro(FILE *out, int verbose) static void print_intro(FILE *out, int verbose)
{ {
general_settings_t *gs = (general_settings_t*) &radio_mem[OFFSET_SETTINGS]; general_settings_t *gs = GET_SETTINGS();
if (verbose) if (verbose)
fprintf(out, "\n# Text displayed when the radio powers up.\n"); fprintf(out, "\n# Text displayed when the radio powers up.\n");
@ -749,7 +757,7 @@ static int have_channels(int mode)
int i; int i;
for (i=0; i<NCHAN; i++) { for (i=0; i<NCHAN; i++) {
channel_t *ch = (channel_t*) &radio_mem[OFFSET_CHANNELS + i*64]; channel_t *ch = GET_CHANNEL(i);
if (ch->name[0] != 0 && ch->channel_mode == mode) if (ch->name[0] != 0 && ch->channel_mode == mode)
return 1; return 1;
@ -850,7 +858,7 @@ static void print_digital_channels(FILE *out, int verbose)
#endif #endif
fprintf(out, "\n"); fprintf(out, "\n");
for (i=0; i<NCHAN; i++) { for (i=0; i<NCHAN; i++) {
channel_t *ch = (channel_t*) &radio_mem[OFFSET_CHANNELS + i*64]; channel_t *ch = GET_CHANNEL(i);
if (ch->name[0] == 0 || ch->channel_mode != MODE_DIGITAL) { if (ch->name[0] == 0 || ch->channel_mode != MODE_DIGITAL) {
// Select digital channels // Select digital channels
@ -939,7 +947,7 @@ static void print_analog_channels(FILE *out, int verbose)
#endif #endif
fprintf(out, "\n"); fprintf(out, "\n");
for (i=0; i<NCHAN; i++) { for (i=0; i<NCHAN; i++) {
channel_t *ch = (channel_t*) &radio_mem[OFFSET_CHANNELS + i*64]; channel_t *ch = GET_CHANNEL(i);
if (ch->name[0] == 0 || ch->channel_mode != MODE_ANALOG) { if (ch->name[0] == 0 || ch->channel_mode != MODE_ANALOG) {
// Select analog channels // Select analog channels
@ -975,37 +983,66 @@ static void print_analog_channels(FILE *out, int verbose)
static int have_zones() static int have_zones()
{ {
zone_t *z = (zone_t*) &radio_mem[OFFSET_ZONES]; int i;
return z->name[0] != 0 && z->name[0] != 0xffff; for (i=0; i<NZONES; i++) {
zone_t *z = GET_ZONE(i);
if (z->name[0] != 0 && z->name[0] != 0xffff)
return 1;
}
return 0;
} }
static int have_scanlists() static int have_scanlists()
{ {
scanlist_t *sl = (scanlist_t*) &radio_mem[OFFSET_SCANL]; int i;
return sl->name[0] != 0 && sl->name[0] != 0xffff; for (i=0; i<NSCANL; i++) {
scanlist_t *sl = GET_SCANLIST(i);
if (sl->name[0] != 0 && sl->name[0] != 0xffff)
return 1;
}
return 0;
} }
static int have_contacts() static int have_contacts()
{ {
contact_t *ct = (contact_t*) &radio_mem[OFFSET_CONTACTS]; int i;
return ct->name[0] != 0 && ct->name[0] != 0xffff; for (i=0; i<NCONTACTS; i++) {
contact_t *ct = GET_CONTACT(i);
if (ct->name[0] != 0 && ct->name[0] != 0xffff)
return 1;
}
return 0;
} }
static int have_grouplists() static int have_grouplists()
{ {
grouplist_t *gl = (grouplist_t*) &radio_mem[OFFSET_GLISTS]; int i;
return gl->name[0] != 0 && gl->name[0] != 0xffff; for (i=0; i<NGLISTS; i++) {
grouplist_t *gl = GET_GROUPLIST(i);
if (gl->name[0] != 0 && gl->name[0] != 0xffff)
return 1;
}
return 0;
} }
static int have_messages() static int have_messages()
{ {
uint16_t *msg = (uint16_t*) &radio_mem[OFFSET_MSG]; int i;
return msg[0] != 0 && msg[0] != 0xffff; for (i=0; i<NMESSAGES; i++) {
uint16_t *msg = GET_MESSAGE(i);
if (msg[0] != 0 && msg[0] != 0xffff)
return 1;
}
return 0;
} }
// //
@ -1045,7 +1082,7 @@ static void md380_print_config(radio_device_t *radio, FILE *out, int verbose)
} }
fprintf(out, "Zone Name Channels\n"); fprintf(out, "Zone Name Channels\n");
for (i=0; i<NZONES; i++) { for (i=0; i<NZONES; i++) {
zone_t *z = (zone_t*) &radio_mem[OFFSET_ZONES + i*64]; zone_t *z = GET_ZONE(i);
if (z->name[0] == 0 || z->name[0] == 0xffff) { if (z->name[0] == 0 || z->name[0] == 0xffff) {
// Zone is disabled. // Zone is disabled.
@ -1085,7 +1122,7 @@ static void md380_print_config(radio_device_t *radio, FILE *out, int verbose)
#endif #endif
fprintf(out, "Channels\n"); fprintf(out, "Channels\n");
for (i=0; i<NSCANL; i++) { for (i=0; i<NSCANL; i++) {
scanlist_t *sl = (scanlist_t*) &radio_mem[OFFSET_SCANL + i*104]; scanlist_t *sl = GET_SCANLIST(i);
if (sl->name[0] == 0 || sl->name[0] == 0xffff) { if (sl->name[0] == 0 || sl->name[0] == 0xffff) {
// Scan list is disabled. // Scan list is disabled.
@ -1144,7 +1181,7 @@ static void md380_print_config(radio_device_t *radio, FILE *out, int verbose)
} }
fprintf(out, "Contact Name Type ID RxTone\n"); fprintf(out, "Contact Name Type ID RxTone\n");
for (i=0; i<NCONTACTS; i++) { for (i=0; i<NCONTACTS; i++) {
contact_t *ct = (contact_t*) &radio_mem[OFFSET_CONTACTS + i*36]; contact_t *ct = GET_CONTACT(i);
if (ct->name[0] == 0 || ct->name[0] == 0xffff) { if (ct->name[0] == 0 || ct->name[0] == 0xffff) {
// Contact is disabled // Contact is disabled
@ -1172,7 +1209,7 @@ static void md380_print_config(radio_device_t *radio, FILE *out, int verbose)
} }
fprintf(out, "Grouplist Name Contacts\n"); fprintf(out, "Grouplist Name Contacts\n");
for (i=0; i<NGLISTS; i++) { for (i=0; i<NGLISTS; i++) {
grouplist_t *gl = (grouplist_t*) &radio_mem[OFFSET_GLISTS + i*96]; grouplist_t *gl = GET_GROUPLIST(i);
if (gl->name[0] == 0 || gl->name[0] == 0xffff) { if (gl->name[0] == 0 || gl->name[0] == 0xffff) {
// Group list is disabled. // Group list is disabled.
@ -1204,7 +1241,7 @@ static void md380_print_config(radio_device_t *radio, FILE *out, int verbose)
} }
fprintf(out, "Message Text\n"); fprintf(out, "Message Text\n");
for (i=0; i<NMESSAGES; i++) { for (i=0; i<NMESSAGES; i++) {
uint16_t *msg = (uint16_t*) &radio_mem[OFFSET_MSG + i*288]; uint16_t *msg = GET_MESSAGE(i);
if (msg[0] == 0 || msg[0] == 0xffff) { if (msg[0] == 0 || msg[0] == 0xffff) {
// Message is disabled // Message is disabled
@ -1318,7 +1355,7 @@ static void erase_contacts()
// //
static void md380_parse_parameter(radio_device_t *radio, char *param, char *value) static void md380_parse_parameter(radio_device_t *radio, char *param, char *value)
{ {
general_settings_t *gs = (general_settings_t*) &radio_mem[OFFSET_SETTINGS]; general_settings_t *gs = GET_SETTINGS();
if (strcasecmp("Radio", param) == 0) { if (strcasecmp("Radio", param) == 0) {
if (strcasecmp(radio->name, value) != 0) { if (strcasecmp(radio->name, value) != 0) {
@ -2057,7 +2094,7 @@ static int md380_parse_row(radio_device_t *radio, int table_id, int first_row, c
// //
static void md380_update_timestamp(radio_device_t *radio) static void md380_update_timestamp(radio_device_t *radio)
{ {
unsigned char *timestamp = &radio_mem[OFFSET_TIMESTMP]; unsigned char *timestamp = GET_TIMESTAMP();
char p[16]; char p[16];
// Last Programmed Date // Last Programmed Date
@ -2085,6 +2122,146 @@ static void md380_update_timestamp(radio_device_t *radio)
} }
} }
//
// Check that configuration is correct.
// Return 0 on error.
//
static int md380_verify_config(radio_device_t *radio)
{
int i, k, nchannels = 0, nzones = 0, nscanlists = 0, ngrouplists = 0;
int ncontacts = 0, nerrors = 0;
// Channels: check references to scanlists, contacts and grouplists.
for (i=0; i<NCHAN; i++) {
channel_t *ch = GET_CHANNEL(i);
if (ch->name[0] == 0 || ch->name[0] == 0xffff)
continue;
nchannels++;
if (ch->scan_list_index != 0) {
scanlist_t *sl = GET_SCANLIST(ch->scan_list_index - 1);
if (sl->name[0] == 0 || sl->name[0] == 0xffff) {
fprintf(stderr, "Channel %d '", i+1);
print_unicode(stderr, ch->name, 16, 0);
fprintf(stderr, "': scanlist %d not found.\n", ch->scan_list_index);
nerrors++;
}
}
if (ch->contact_name_index != 0) {
contact_t *ct = GET_CONTACT(ch->contact_name_index - 1);
if (ct->name[0] == 0 || ct->name[0] == 0xffff) {
fprintf(stderr, "Channel %d '", i+1);
print_unicode(stderr, ch->name, 16, 0);
fprintf(stderr, "': contact %d not found.\n", ch->contact_name_index);
nerrors++;
}
}
if (ch->group_list_index != 0) {
grouplist_t *gl = GET_GROUPLIST(ch->group_list_index - 1);
if (gl->name[0] == 0 || gl->name[0] == 0xffff) {
fprintf(stderr, "Channel %d '", i+1);
print_unicode(stderr, ch->name, 16, 0);
fprintf(stderr, "': grouplist %d not found.\n", ch->group_list_index);
nerrors++;
}
}
}
// Zones: check references to channels.
for (i=0; i<NZONES; i++) {
zone_t *z = GET_ZONE(i);
if (z->name[0] == 0 || z->name[0] == 0xffff)
continue;
nzones++;
for (k=0; k<16; k++) {
int cnum = z->member[k];
if (cnum != 0) {
channel_t *ch = GET_CHANNEL(cnum - 1);
if (ch->name[0] == 0 || ch->name[0] == 0xffff) {
fprintf(stderr, "Zone %da '", i+1);
print_unicode(stderr, z->name, 16, 0);
fprintf(stderr, "': channel %d not found.\n", cnum);
nerrors++;
}
}
}
}
// Scanlists: check references to channels.
for (i=0; i<NSCANL; i++) {
scanlist_t *sl = GET_SCANLIST(i);
if (sl->name[0] == 0 || sl->name[0] == 0xffff)
continue;
nscanlists++;
for (k=0; k<31; k++) {
int cnum = sl->member[k];
if (cnum != 0) {
channel_t *ch = GET_CHANNEL(cnum - 1);
if (ch->name[0] == 0 || ch->name[0] == 0xffff) {
fprintf(stderr, "Scanlist %d '", i+1);
print_unicode(stderr, sl->name, 16, 0);
fprintf(stderr, "': channel %d not found.\n", cnum);
nerrors++;
}
}
}
}
// Grouplists: check references to contacts.
for (i=0; i<NGLISTS; i++) {
grouplist_t *gl = GET_GROUPLIST(i);
if (gl->name[0] == 0 || gl->name[0] == 0xffff)
continue;
ngrouplists++;
for (k=0; k<32; k++) {
int cnum = gl->member[k];
if (cnum != 0) {
contact_t *ct = GET_CONTACT(cnum - 1);
if (ct->name[0] == 0 || ct->name[0] == 0xffff) {
fprintf(stderr, "Grouplist %d '", i+1);
print_unicode(stderr, gl->name, 16, 0);
fprintf(stderr, "': contact %d not found.\n", cnum);
nerrors++;
}
}
}
}
// Count contacts.
for (i=0; i<NCONTACTS; i++) {
contact_t *ct = GET_CONTACT(i);
if (ct->name[0] == 0 || ct->name[0] == 0xffff)
continue;
ncontacts++;
}
if (nerrors > 0) {
fprintf(stderr, "Total %d errors.\n", nerrors);
return 0;
}
fprintf(stderr, "Total %d channels, %d zones, %d scanlists, %d contacts, %d grouplists.\n",
nchannels, nzones, nscanlists, ncontacts, ngrouplists);
return 1;
}
// //
// TYT MD-380 // TYT MD-380
// //
@ -2097,6 +2274,7 @@ radio_device_t radio_md380 = {
md380_save_image, md380_save_image,
md380_print_version, md380_print_version,
md380_print_config, md380_print_config,
md380_verify_config,
md380_parse_parameter, md380_parse_parameter,
md380_parse_header, md380_parse_header,
md380_parse_row, md380_parse_row,

11
radio.c
View File

@ -282,3 +282,14 @@ void radio_print_config(FILE *out, int verbose)
} }
device->print_config(device, out, verbose); device->print_config(device, out, verbose);
} }
//
// Check the configuration is correct.
//
void radio_verify_config()
{
if (!device->verify_config(device)) {
// Message should be already printed.
exit(-1);
}
}

View File

@ -72,6 +72,11 @@ void radio_save_image(char *filename);
// //
void radio_parse_config(char *filename); void radio_parse_config(char *filename);
//
// Check the configuration.
//
void radio_verify_config();
// //
// Device-dependent interface to the radio. // Device-dependent interface to the radio.
// //
@ -85,6 +90,7 @@ struct _radio_device_t {
void (*save_image)(radio_device_t *radio, FILE *img); void (*save_image)(radio_device_t *radio, FILE *img);
void (*print_version)(radio_device_t *radio, FILE *out); void (*print_version)(radio_device_t *radio, FILE *out);
void (*print_config)(radio_device_t *radio, FILE *out, int verbose); void (*print_config)(radio_device_t *radio, FILE *out, int verbose);
int (*verify_config)(radio_device_t *radio);
void (*parse_parameter)(radio_device_t *radio, char *param, char *value); void (*parse_parameter)(radio_device_t *radio, char *param, char *value);
int (*parse_header)(radio_device_t *radio, char *line); int (*parse_header)(radio_device_t *radio, char *line);
int (*parse_row)(radio_device_t *radio, int table_id, int first_row, char *line); int (*parse_row)(radio_device_t *radio, int table_id, int first_row, char *line);

298
uv380.c
View File

@ -52,6 +52,16 @@
#define OFFSET_CHANNELS 0x40000 #define OFFSET_CHANNELS 0x40000
#define OFFSET_CONTACTS 0x70000 #define OFFSET_CONTACTS 0x70000
#define GET_TIMESTAMP() (&radio_mem[OFFSET_TIMESTMP])
#define GET_SETTINGS() ((general_settings_t*) &radio_mem[OFFSET_SETTINGS])
#define GET_CHANNEL(i) ((channel_t*) &radio_mem[OFFSET_CHANNELS + (i)*64])
#define GET_ZONE(i) ((zone_t*) &radio_mem[OFFSET_ZONES + (i)*64])
#define GET_ZONEXT(i) ((zone_ext_t*) &radio_mem[OFFSET_ZONEXT + (i)*224])
#define GET_SCANLIST(i) ((scanlist_t*) &radio_mem[OFFSET_SCANL + (i)*104])
#define GET_CONTACT(i) ((contact_t*) &radio_mem[OFFSET_CONTACTS + (i)*36])
#define GET_GROUPLIST(i) ((grouplist_t*) &radio_mem[OFFSET_GLISTS + (i)*96])
#define GET_MESSAGE(i) ((uint16_t*) &radio_mem[OFFSET_MSG + (i)*288])
// //
// Channel data. // Channel data.
// //
@ -343,7 +353,7 @@ static const char *TURNOFF_FREQ[] = { "259.2", "55.2", "???", "-" };
// //
static void uv380_print_version(radio_device_t *radio, FILE *out) static void uv380_print_version(radio_device_t *radio, FILE *out)
{ {
unsigned char *timestamp = &radio_mem[OFFSET_TIMESTMP]; unsigned char *timestamp = GET_TIMESTAMP();
static const char charmap[16] = "0123456789:;<=>?"; static const char charmap[16] = "0123456789:;<=>?";
if (*timestamp != 0xff) { if (*timestamp != 0xff) {
@ -410,7 +420,7 @@ static int uv380_is_compatible(radio_device_t *radio)
// //
static void setup_zone(int index, const char *name) static void setup_zone(int index, const char *name)
{ {
zone_t *z = (zone_t*) &radio_mem[OFFSET_ZONES + index*64]; zone_t *z = GET_ZONE(index);
utf8_decode(z->name, name, 16); utf8_decode(z->name, name, 16);
} }
@ -419,10 +429,10 @@ static void setup_zone(int index, const char *name)
// Add channel to a zone. // Add channel to a zone.
// Return 0 on failure. // Return 0 on failure.
// //
static int zone_append(int zone_index, int b_flag, int cnum) static int zone_append(int index, int b_flag, int cnum)
{ {
zone_t *z = (zone_t*) &radio_mem[OFFSET_ZONES + zone_index*64]; zone_t *z = GET_ZONE(index);
zone_ext_t *zext = (zone_ext_t*) &radio_mem[OFFSET_ZONEXT + zone_index*224]; zone_ext_t *zext = GET_ZONEXT(index);
int i; int i;
if (b_flag) { if (b_flag) {
@ -455,10 +465,10 @@ static int zone_append(int zone_index, int b_flag, int cnum)
return 0; return 0;
} }
static void erase_zone(int zone_index) static void erase_zone(int index)
{ {
zone_t *z = (zone_t*) &radio_mem[OFFSET_ZONES + zone_index*64]; zone_t *z = GET_ZONE(index);
zone_ext_t *zext = (zone_ext_t*) &radio_mem[OFFSET_ZONEXT + zone_index*224]; zone_ext_t *zext = GET_ZONEXT(index);
memset(z, 0, 64); memset(z, 0, 64);
memset(zext, 0, 224); memset(zext, 0, 224);
@ -470,7 +480,7 @@ static void erase_zone(int zone_index)
static void setup_scanlist(int index, const char *name, static void setup_scanlist(int index, const char *name,
int prio1, int prio2, int txchan) int prio1, int prio2, int txchan)
{ {
scanlist_t *sl = (scanlist_t*) &radio_mem[OFFSET_SCANL + index*104]; scanlist_t *sl = GET_SCANLIST(index);
// Bytes 0-31 // Bytes 0-31
utf8_decode(sl->name, name, 16); utf8_decode(sl->name, name, 16);
@ -483,7 +493,7 @@ static void setup_scanlist(int index, const char *name,
static void erase_scanlist(int index) static void erase_scanlist(int index)
{ {
scanlist_t *sl = (scanlist_t*) &radio_mem[OFFSET_SCANL + index*104]; scanlist_t *sl = GET_SCANLIST(index);
memset(sl, 0, 104); memset(sl, 0, 104);
@ -503,9 +513,9 @@ static void erase_scanlist(int index)
// Add channel to a zone. // Add channel to a zone.
// Return 0 on failure. // Return 0 on failure.
// //
static int scanlist_append(int list_index, int cnum) static int scanlist_append(int index, int cnum)
{ {
scanlist_t *sl = (scanlist_t*) &radio_mem[OFFSET_SCANL + list_index*104]; scanlist_t *sl = GET_SCANLIST(index);
int i; int i;
for (i=0; i<31; i++) { for (i=0; i<31; i++) {
@ -521,7 +531,7 @@ static int scanlist_append(int list_index, int cnum)
static void erase_contact(int index) static void erase_contact(int index)
{ {
contact_t *ct = (contact_t*) &radio_mem[OFFSET_CONTACTS + index*36]; contact_t *ct = GET_CONTACT(index);
memset(ct, 0, 36); memset(ct, 0, 36);
*(uint32_t*)ct = 0xffffffff; *(uint32_t*)ct = 0xffffffff;
@ -529,7 +539,7 @@ static void erase_contact(int index)
static void setup_contact(int index, const char *name, int type, int id, int rxtone) static void setup_contact(int index, const char *name, int type, int id, int rxtone)
{ {
contact_t *ct = (contact_t*) &radio_mem[OFFSET_CONTACTS + index*36]; contact_t *ct = GET_CONTACT(index);
ct->id = id; ct->id = id;
ct->type = type; ct->type = type;
@ -540,7 +550,7 @@ static void setup_contact(int index, const char *name, int type, int id, int rxt
static void setup_grouplist(int index, const char *name) static void setup_grouplist(int index, const char *name)
{ {
grouplist_t *gl = (grouplist_t*) &radio_mem[OFFSET_GLISTS + index*96]; grouplist_t *gl = GET_GROUPLIST(index);
utf8_decode(gl->name, name, 16); utf8_decode(gl->name, name, 16);
} }
@ -551,7 +561,7 @@ static void setup_grouplist(int index, const char *name)
// //
static int grouplist_append(int index, int cnum) static int grouplist_append(int index, int cnum)
{ {
grouplist_t *gl = (grouplist_t*) &radio_mem[OFFSET_GLISTS + index*96]; grouplist_t *gl = GET_GROUPLIST(index);
int i; int i;
for (i=0; i<32; i++) { for (i=0; i<32; i++) {
@ -570,7 +580,7 @@ static int grouplist_append(int index, int cnum)
// //
static void setup_message(int index, const char *text) static void setup_message(int index, const char *text)
{ {
uint16_t *msg = (uint16_t*) &radio_mem[OFFSET_MSG + index*288]; uint16_t *msg = GET_MESSAGE(index);
// Skip spaces and tabs. // Skip spaces and tabs.
while (*text == ' ' || *text == '\t') while (*text == ' ' || *text == '\t')
@ -598,7 +608,7 @@ static void setup_channel(int i, int mode, char *name, double rx_mhz, double tx_
int admit, int colorcode, int timeslot, int incall, int grouplist, int contact, int admit, int colorcode, int timeslot, int incall, int grouplist, int contact,
int rxtone, int txtone, int width) int rxtone, int txtone, int width)
{ {
channel_t *ch = (channel_t*) &radio_mem[OFFSET_CHANNELS + i*64]; channel_t *ch = GET_CHANNEL(i);
ch->channel_mode = mode; ch->channel_mode = mode;
ch->bandwidth = width; ch->bandwidth = width;
@ -628,7 +638,7 @@ static void setup_channel(int i, int mode, char *name, double rx_mhz, double tx_
// //
static void erase_channel(int i) static void erase_channel(int i)
{ {
channel_t *ch = (channel_t*) &radio_mem[OFFSET_CHANNELS + i*64]; channel_t *ch = GET_CHANNEL(i);
// Byte 0 // Byte 0
ch->channel_mode = MODE_ANALOG; ch->channel_mode = MODE_ANALOG;
@ -748,7 +758,7 @@ static void print_chanlist(FILE *out, uint16_t *unsorted, int nchan)
static void print_id(FILE *out, int verbose) static void print_id(FILE *out, int verbose)
{ {
general_settings_t *gs = (general_settings_t*) &radio_mem[OFFSET_SETTINGS]; general_settings_t *gs = GET_SETTINGS();
unsigned id = gs->radio_id[0] | (gs->radio_id[1] << 8) | (gs->radio_id[2] << 16); unsigned id = gs->radio_id[0] | (gs->radio_id[1] << 8) | (gs->radio_id[2] << 16);
if (verbose) if (verbose)
@ -764,7 +774,7 @@ static void print_id(FILE *out, int verbose)
static void print_intro(FILE *out, int verbose) static void print_intro(FILE *out, int verbose)
{ {
general_settings_t *gs = (general_settings_t*) &radio_mem[OFFSET_SETTINGS]; general_settings_t *gs = GET_SETTINGS();
if (verbose) if (verbose)
fprintf(out, "\n# Text displayed when the radio powers up.\n"); fprintf(out, "\n# Text displayed when the radio powers up.\n");
@ -791,7 +801,7 @@ static int have_channels(int mode)
int i; int i;
for (i=0; i<NCHAN; i++) { for (i=0; i<NCHAN; i++) {
channel_t *ch = (channel_t*) &radio_mem[OFFSET_CHANNELS + i*64]; channel_t *ch = GET_CHANNEL(i);
if (ch->name[0] != 0 && ch->channel_mode == mode) if (ch->name[0] != 0 && ch->channel_mode == mode)
return 1; return 1;
@ -894,7 +904,7 @@ static void print_digital_channels(FILE *out, int verbose)
#endif #endif
fprintf(out, "\n"); fprintf(out, "\n");
for (i=0; i<NCHAN; i++) { for (i=0; i<NCHAN; i++) {
channel_t *ch = (channel_t*) &radio_mem[OFFSET_CHANNELS + i*64]; channel_t *ch = GET_CHANNEL(i);
if (ch->name[0] == 0 || ch->channel_mode != MODE_DIGITAL) { if (ch->name[0] == 0 || ch->channel_mode != MODE_DIGITAL) {
// Select digital channels // Select digital channels
@ -987,7 +997,7 @@ static void print_analog_channels(FILE *out, int verbose)
#endif #endif
fprintf(out, "\n"); fprintf(out, "\n");
for (i=0; i<NCHAN; i++) { for (i=0; i<NCHAN; i++) {
channel_t *ch = (channel_t*) &radio_mem[OFFSET_CHANNELS + i*64]; channel_t *ch = GET_CHANNEL(i);
if (ch->name[0] == 0 || ch->channel_mode != MODE_ANALOG) { if (ch->name[0] == 0 || ch->channel_mode != MODE_ANALOG) {
// Select analog channels // Select analog channels
@ -1024,37 +1034,66 @@ static void print_analog_channels(FILE *out, int verbose)
static int have_zones() static int have_zones()
{ {
zone_t *z = (zone_t*) &radio_mem[OFFSET_ZONES]; int i;
return z->name[0] != 0 && z->name[0] != 0xffff; for (i=0; i<NZONES; i++) {
zone_t *z = GET_ZONE(i);
if (z->name[0] != 0 && z->name[0] != 0xffff)
return 1;
}
return 0;
} }
static int have_scanlists() static int have_scanlists()
{ {
scanlist_t *sl = (scanlist_t*) &radio_mem[OFFSET_SCANL]; int i;
return sl->name[0] != 0 && sl->name[0] != 0xffff; for (i=0; i<NSCANL; i++) {
scanlist_t *sl = GET_SCANLIST(i);
if (sl->name[0] != 0 && sl->name[0] != 0xffff)
return 1;
}
return 0;
} }
static int have_contacts() static int have_contacts()
{ {
contact_t *ct = (contact_t*) &radio_mem[OFFSET_CONTACTS]; int i;
return ct->name[0] != 0 && ct->name[0] != 0xffff; for (i=0; i<NCONTACTS; i++) {
contact_t *ct = GET_CONTACT(i);
if (ct->name[0] != 0 && ct->name[0] != 0xffff)
return 1;
}
return 0;
} }
static int have_grouplists() static int have_grouplists()
{ {
grouplist_t *gl = (grouplist_t*) &radio_mem[OFFSET_GLISTS]; int i;
return gl->name[0] != 0 && gl->name[0] != 0xffff; for (i=0; i<NGLISTS; i++) {
grouplist_t *gl = GET_GROUPLIST(i);
if (gl->name[0] != 0 && gl->name[0] != 0xffff)
return 1;
}
return 0;
} }
static int have_messages() static int have_messages()
{ {
uint16_t *msg = (uint16_t*) &radio_mem[OFFSET_MSG]; int i;
return msg[0] != 0 && msg[0] != 0xffff; for (i=0; i<NMESSAGES; i++) {
uint16_t *msg = GET_MESSAGE(i);
if (msg[0] != 0 && msg[0] != 0xffff)
return 1;
}
return 0;
} }
// //
@ -1094,8 +1133,8 @@ static void uv380_print_config(radio_device_t *radio, FILE *out, int verbose)
} }
fprintf(out, "Zone Name Channels\n"); fprintf(out, "Zone Name Channels\n");
for (i=0; i<NZONES; i++) { for (i=0; i<NZONES; i++) {
zone_t *z = (zone_t*) &radio_mem[OFFSET_ZONES + i*64]; zone_t *z = GET_ZONE(i);
zone_ext_t *zext = (zone_ext_t*) &radio_mem[OFFSET_ZONEXT + i*224]; zone_ext_t *zext = GET_ZONEXT(i);
if (z->name[0] == 0 || z->name[0] == 0xffff) { if (z->name[0] == 0 || z->name[0] == 0xffff) {
// Zone is disabled. // Zone is disabled.
@ -1147,7 +1186,7 @@ static void uv380_print_config(radio_device_t *radio, FILE *out, int verbose)
#endif #endif
fprintf(out, "Channels\n"); fprintf(out, "Channels\n");
for (i=0; i<NSCANL; i++) { for (i=0; i<NSCANL; i++) {
scanlist_t *sl = (scanlist_t*) &radio_mem[OFFSET_SCANL + i*104]; scanlist_t *sl = GET_SCANLIST(i);
if (sl->name[0] == 0 || sl->name[0] == 0xffff) { if (sl->name[0] == 0 || sl->name[0] == 0xffff) {
// Scan list is disabled. // Scan list is disabled.
@ -1206,7 +1245,7 @@ static void uv380_print_config(radio_device_t *radio, FILE *out, int verbose)
} }
fprintf(out, "Contact Name Type ID RxTone\n"); fprintf(out, "Contact Name Type ID RxTone\n");
for (i=0; i<NCONTACTS; i++) { for (i=0; i<NCONTACTS; i++) {
contact_t *ct = (contact_t*) &radio_mem[OFFSET_CONTACTS + i*36]; contact_t *ct = GET_CONTACT(i);
if (ct->name[0] == 0 || ct->name[0] == 0xffff) { if (ct->name[0] == 0 || ct->name[0] == 0xffff) {
// Contact is disabled // Contact is disabled
@ -1234,7 +1273,7 @@ static void uv380_print_config(radio_device_t *radio, FILE *out, int verbose)
} }
fprintf(out, "Grouplist Name Contacts\n"); fprintf(out, "Grouplist Name Contacts\n");
for (i=0; i<NGLISTS; i++) { for (i=0; i<NGLISTS; i++) {
grouplist_t *gl = (grouplist_t*) &radio_mem[OFFSET_GLISTS + i*96]; grouplist_t *gl = GET_GROUPLIST(i);
if (gl->name[0] == 0 || gl->name[0] == 0xffff) { if (gl->name[0] == 0 || gl->name[0] == 0xffff) {
// Group list is disabled. // Group list is disabled.
@ -1266,7 +1305,7 @@ static void uv380_print_config(radio_device_t *radio, FILE *out, int verbose)
} }
fprintf(out, "Message Text\n"); fprintf(out, "Message Text\n");
for (i=0; i<NMESSAGES; i++) { for (i=0; i<NMESSAGES; i++) {
uint16_t *msg = (uint16_t*) &radio_mem[OFFSET_MSG + i*288]; uint16_t *msg = GET_MESSAGE(i);
if (msg[0] == 0 || msg[0] == 0xffff) { if (msg[0] == 0 || msg[0] == 0xffff) {
// Message is disabled // Message is disabled
@ -1385,7 +1424,7 @@ static void erase_contacts()
// //
static void uv380_parse_parameter(radio_device_t *radio, char *param, char *value) static void uv380_parse_parameter(radio_device_t *radio, char *param, char *value)
{ {
general_settings_t *gs = (general_settings_t*) &radio_mem[OFFSET_SETTINGS]; general_settings_t *gs = GET_SETTINGS();
if (strcasecmp("Radio", param) == 0) { if (strcasecmp("Radio", param) == 0) {
// Accept either MD-2017 or MD-UV380. // Accept either MD-2017 or MD-UV380.
@ -2128,7 +2167,7 @@ static int uv380_parse_row(radio_device_t *radio, int table_id, int first_row, c
// //
static void uv380_update_timestamp(radio_device_t *radio) static void uv380_update_timestamp(radio_device_t *radio)
{ {
unsigned char *timestamp = &radio_mem[OFFSET_TIMESTMP]; unsigned char *timestamp = GET_TIMESTAMP();
char p[16]; char p[16];
// Last Programmed Date // Last Programmed Date
@ -2156,6 +2195,179 @@ static void uv380_update_timestamp(radio_device_t *radio)
} }
} }
//
// Check that configuration is correct.
// Return 0 on error.
//
static int uv380_verify_config(radio_device_t *radio)
{
int i, k, nchannels = 0, nzones = 0, nscanlists = 0, ngrouplists = 0;
int ncontacts = 0, nerrors = 0;
// Channels: check references to scanlists, contacts and grouplists.
for (i=0; i<NCHAN; i++) {
channel_t *ch = GET_CHANNEL(i);
if (ch->name[0] == 0 || ch->name[0] == 0xffff)
continue;
nchannels++;
if (ch->scan_list_index != 0) {
scanlist_t *sl = GET_SCANLIST(ch->scan_list_index - 1);
if (sl->name[0] == 0 || sl->name[0] == 0xffff) {
fprintf(stderr, "Channel %d '", i+1);
print_unicode(stderr, ch->name, 16, 0);
fprintf(stderr, "': scanlist %d not found.\n", ch->scan_list_index);
nerrors++;
}
}
if (ch->contact_name_index != 0) {
contact_t *ct = GET_CONTACT(ch->contact_name_index - 1);
if (ct->name[0] == 0 || ct->name[0] == 0xffff) {
fprintf(stderr, "Channel %d '", i+1);
print_unicode(stderr, ch->name, 16, 0);
fprintf(stderr, "': contact %d not found.\n", ch->contact_name_index);
nerrors++;
}
}
if (ch->group_list_index != 0) {
grouplist_t *gl = GET_GROUPLIST(ch->group_list_index - 1);
if (gl->name[0] == 0 || gl->name[0] == 0xffff) {
fprintf(stderr, "Channel %d '", i+1);
print_unicode(stderr, ch->name, 16, 0);
fprintf(stderr, "': grouplist %d not found.\n", ch->group_list_index);
nerrors++;
}
}
}
// Zones: check references to channels.
for (i=0; i<NZONES; i++) {
zone_t *z = GET_ZONE(i);
zone_ext_t *zext = GET_ZONEXT(i);
if (z->name[0] == 0 || z->name[0] == 0xffff)
continue;
nzones++;
// Zone A
for (k=0; k<16; k++) {
int cnum = z->member_a[k];
if (cnum != 0) {
channel_t *ch = GET_CHANNEL(cnum - 1);
if (ch->name[0] == 0 || ch->name[0] == 0xffff) {
fprintf(stderr, "Zone %da '", i+1);
print_unicode(stderr, z->name, 16, 0);
fprintf(stderr, "': channel %d not found.\n", cnum);
nerrors++;
}
}
}
for (k=0; k<48; k++) {
int cnum = zext->ext_a[k];
if (cnum != 0) {
channel_t *ch = GET_CHANNEL(cnum - 1);
if (ch->name[0] == 0 || ch->name[0] == 0xffff) {
fprintf(stderr, "Zone %da '", i+1);
print_unicode(stderr, z->name, 16, 0);
fprintf(stderr, "': channel %d not found.\n", cnum);
nerrors++;
}
}
}
// Zone B
for (k=0; k<64; k++) {
int cnum = zext->member_b[k];
if (cnum != 0) {
channel_t *ch = GET_CHANNEL(cnum - 1);
if (ch->name[0] == 0 || ch->name[0] == 0xffff) {
fprintf(stderr, "Zone %db '", i+1);
print_unicode(stderr, z->name, 16, 0);
fprintf(stderr, "': channel %d not found.\n", cnum);
nerrors++;
}
}
}
}
// Scanlists: check references to channels.
for (i=0; i<NSCANL; i++) {
scanlist_t *sl = GET_SCANLIST(i);
if (sl->name[0] == 0 || sl->name[0] == 0xffff)
continue;
nscanlists++;
for (k=0; k<31; k++) {
int cnum = sl->member[k];
if (cnum != 0) {
channel_t *ch = GET_CHANNEL(cnum - 1);
if (ch->name[0] == 0 || ch->name[0] == 0xffff) {
fprintf(stderr, "Scanlist %d '", i+1);
print_unicode(stderr, sl->name, 16, 0);
fprintf(stderr, "': channel %d not found.\n", cnum);
nerrors++;
}
}
}
}
// Grouplists: check references to contacts.
for (i=0; i<NGLISTS; i++) {
grouplist_t *gl = GET_GROUPLIST(i);
if (gl->name[0] == 0 || gl->name[0] == 0xffff)
continue;
ngrouplists++;
for (k=0; k<32; k++) {
int cnum = gl->member[k];
if (cnum != 0) {
contact_t *ct = GET_CONTACT(cnum - 1);
if (ct->name[0] == 0 || ct->name[0] == 0xffff) {
fprintf(stderr, "Grouplist %d '", i+1);
print_unicode(stderr, gl->name, 16, 0);
fprintf(stderr, "': contact %d not found.\n", cnum);
nerrors++;
}
}
}
}
// Count contacts.
for (i=0; i<NCONTACTS; i++) {
contact_t *ct = GET_CONTACT(i);
if (ct->name[0] == 0 || ct->name[0] == 0xffff)
continue;
ncontacts++;
}
if (nerrors > 0) {
fprintf(stderr, "Total %d errors.\n", nerrors);
return 0;
}
fprintf(stderr, "Total %d channels, %d zones, %d scanlists, %d contacts, %d grouplists.\n",
nchannels, nzones, nscanlists, ncontacts, ngrouplists);
return 1;
}
// //
// TYT MD-UV380 // TYT MD-UV380
// //
@ -2168,6 +2380,7 @@ radio_device_t radio_uv380 = {
uv380_save_image, uv380_save_image,
uv380_print_version, uv380_print_version,
uv380_print_config, uv380_print_config,
uv380_verify_config,
uv380_parse_parameter, uv380_parse_parameter,
uv380_parse_header, uv380_parse_header,
uv380_parse_row, uv380_parse_row,
@ -2186,6 +2399,7 @@ radio_device_t radio_md2017 = {
uv380_save_image, uv380_save_image,
uv380_print_version, uv380_print_version,
uv380_print_config, uv380_print_config,
uv380_verify_config,
uv380_parse_parameter, uv380_parse_parameter,
uv380_parse_header, uv380_parse_header,
uv380_parse_row, uv380_parse_row,