/* * Interface to Anytone D868UV. * * Copyright (C) 2018 Serge Vakulenko, KK6ABQ * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include "radio.h" #include "util.h" // // Sizes of configuration tables. // #define NCHAN 4000 #define NCONTACTS 10000 #define NZONES 250 #define NGLISTS 250 #define NSCANL 250 #define NMESSAGES 100 #define NCALLSIGNS 160000 #define CALLSIGN_SIZE (12*1024*1024) // Size of callsign data // // Offsets in the image file. // #define OFFSET_BANK1 0x000040 // Channels #define OFFSET_ZONELISTS 0x03e8c0 // Channel lists of zones #define OFFSET_SCANLISTS 0x05dcc0 // Scanlists #define OFFSET_MESSAGES 0x069f40 // Messages #define OFFSET_ZONE_MAP 0x070940 // Bitmap of valid zones #define OFFSET_SCANL_MAP 0x070980 // Bitmap of valid scanlists #define OFFSET_CHAN_MAP 0x070a40 // Bitmap of valid channels #define OFFSET_SETTINGS 0x071600 // General settings #define OFFSET_ZCHAN_A 0x071700 // Zone A channel #define OFFSET_ZCHAN_B 0x071900 // Zone B channel #define OFFSET_ZONENAMES 0x071dc0 // Names of zones #define OFFSET_RADIOID 0x073d00 // Table of radio IDs #define OFFSET_CONTACT_LIST 0x076500 // List of valid contact indices #define OFFSET_CONTACT_MAP 0x080140 // Bitmap of invalid contacts #define OFFSET_CONTACTS 0x080640 // Contacts #define OFFSET_GLISTS 0x174b00 // RX group lists // // Addresses in the radio flash memory. // #define ADDR_CALLDB_LIST 0x04000000 // Map of callsign database #define ADDR_CONT_ID_LIST 0x04280000 // Map of contact IDs to contacts #define ADDR_CALLDB_SIZE 0x044c0000 // Sizes of callsign database #define ADDR_CALLDB_DATA 0x04500000 // Data of callsign database #define GET_SETTINGS() ((general_settings_t*) &radio_mem[OFFSET_SETTINGS]) #define GET_RADIOID() ((radioid_t*) &radio_mem[OFFSET_RADIOID]) #define GET_ZONEMAP() (&radio_mem[OFFSET_ZONE_MAP]) #define GET_CONTACT_MAP() (&radio_mem[OFFSET_CONTACT_MAP]) #define GET_CONTACT_LIST() ((uint32_t*) &radio_mem[OFFSET_CONTACT_LIST]) #define GET_SCANL_MAP() (&radio_mem[OFFSET_SCANL_MAP]) #define GET_ZONENAME(i) (&radio_mem[OFFSET_ZONENAMES + (i)*32]) #define GET_ZONE_CHAN_A(i) ((i) + (uint16_t*) &radio_mem[OFFSET_ZCHAN_A]) #define GET_ZONE_CHAN_B(i) ((i) + (uint16_t*) &radio_mem[OFFSET_ZCHAN_B]) #define GET_ZONELIST(i) ((uint16_t*) &radio_mem[OFFSET_ZONELISTS + (i)*512]) #define GET_CONTACT(i) ((contact_t*) &radio_mem[OFFSET_CONTACTS + (i)*100]) #define GET_GROUPLIST(i) ((grouplist_t*) &radio_mem[OFFSET_GLISTS + (i)*320]) #define GET_SCANLIST(i) ((scanlist_t*) &radio_mem[OFFSET_SCANLISTS + (i)*192]) #define GET_MESSAGE(i) ((uint8_t*) &radio_mem[OFFSET_MESSAGES + (i)*256]) #define VALID_TEXT(txt) (*(txt) != 0 && *(txt) != 0xff) #define VALID_GROUPLIST(gl) ((gl)->member[0] != 0xffffffff && VALID_TEXT((gl)->name)) // // Size of memory image. // Essentialy a sum of all fragments defined ind868um-map.h. // #define MEMSZ 1606528 // // D868UV radio has a huge internal address space, more than 64 Mbytes. // The configuration data are dispersed over this space. // Here is a table of fragments: starting address and length. // We read these fragments and save them into a file continuously. // typedef struct { unsigned address; unsigned length; unsigned offset; } fragment_t; static fragment_t region_map[] = { #include "d868uv-map.h" }; // // Channel data. // typedef struct { // Bytes 0-7 uint32_t rx_frequency; // RX Frequency: 8 digits BCD uint32_t tx_offset; // TX Offset: 8 digits BCD // Byte 8 uint8_t channel_mode : 2, // Mode: Analog or Digital #define MODE_ANALOG 0 // Analog #define MODE_DIGITAL 1 // Digital #define MODE_A_D 2 // A+D, transmit analog #define MODE_D_A 3 // D+A, transmit digital power : 2, // Power: Low, Middle, High, Turbo #define POWER_LOW 0 #define POWER_MIDDLE 1 #define POWER_HIGH 2 #define POWER_TURBO 3 bandwidth : 1, // Bandwidth: 12.5 or 25 kHz #define BW_12_5_KHZ 0 #define BW_25_KHZ 1 _unused8 : 1, // 0 repeater_mode : 2; // Sign of TX frequency offset #define RM_SIMPLEX 0 // TX frequency = RX frequency #define RM_TXPOS 1 // Positive TX offset #define RM_TXNEG 2 // Negative TX offset // Byte 9 uint8_t rx_ctcss : 1, // CTCSS Decode rx_dcs : 1, // DCS Decode tx_ctcss : 1, // CTCSS Encode tx_dcs : 1, // DCS Encode reverse : 1, // Reverse rx_only : 1, // TX Prohibit call_confirm : 1, // Call Confirmation talkaround : 1; // Talk Around // Bytes 10-15 uint8_t ctcss_transmit; // CTCSS Encode: 0=62.5, 50=254.1, 51=Define uint8_t ctcss_receive; // CTCSS Decode: 0=62.5, 50=254.1, 51=Define uint16_t dcs_transmit; // DCS Encode: 0=D000N, 17=D021N, 1023=D777I uint16_t dcs_receive; // DCS Decode: 0=D000N, 17=D021N, 1023=D777I // Bytes 16-19 uint16_t custom_ctcss; // 0x09cf=251.1, 0x0a28=260 uint8_t tone2_decode; // 2Tone Decode: 0x00=1, 0x0f=16 uint8_t _unused19; // 0 // Bytes 20-23 uint16_t contact_index; // Contact: 0=Contact1, 1=Contact2, ... uint16_t _unused22; // 0 // Byte 24 uint8_t id_index; // Index in Radio ID table // Byte 25 uint8_t ptt_id : 2, // PTT ID #define PTTID_OFF 0 #define PTTID_START 1 #define PTTID_END 2 #define PTTID_START_END 3 _unused25_1 : 2, // 0 squelch_mode : 1, // Squelch Mode #define SQ_CARRIER 0 // Carrier #define SQ_TONE 1 // CTCSS/DCS _unused25_2 : 3; // 0 // Byte 26 uint8_t tx_permit : 2, // TX Permit #define PERMIT_ALWAYS 0 // Always #define PERMIT_CH_FREE 1 // Channel Free #define PERMIT_CC_DIFF 2 // Different Color Code #define PERMIT_CC_SAME 3 // Same Color Code _unused26_1 : 2, // 0 _opt_signal : 2, // Optional Signal #define OPTSIG_OFF 0 // Off #define OPTSIG_DTMF 1 // DTMF #define OPTSIG_2TONE 2 // 2Tone #define OPTSIG_5TONE 3 // 5Tone _unused26_2 : 2; // 0 // Bytes 27-31 uint8_t scan_list_index; // Scan List: 0xff=None, 0=ScanList1... uint8_t group_list_index; // Receive Group List: 0xff=None, 0=GroupList1... uint8_t id_2tone; // 2Tone ID: 0=1, 0x17=24 uint8_t id_5tone; // 5Tone ID: 0=1, 0x63=100 uint8_t id_dtmf; // DTMF ID: 0=1, 0x0f=16 // Byte 32 uint8_t color_code; // Color Code: 0-15 // Byte 33 uint8_t slot2 : 1, // Slot: Slot2 _unused33_1 : 1, // 0 simplex_tdma : 1, // Simplex TDMA: On _unused33_2 : 1, // 0 tdma_adaptive : 1, // TDMA Adaptive: On _unused33_3 : 1, // 0 enh_encryption : 1, // Encryption Type: Enhanced Encryption work_alone : 1; // Work Alone: On // Byte 34 uint8_t encryption; // Digital Encryption: 1-32, 0=Off // Bytes 35-51 uint8_t name[16]; // Channel Name, zero filled uint8_t _unused51; // 0 // Byte 52 uint8_t ranging : 1, // Ranging: On through_mode : 1, // Through Mode: On _unused52 : 6; // 0 // Byte 53 uint8_t aprs_report : 1, // APRS Report: On _unused53 : 7; // 0 // Bytes 54-63 uint8_t aprs_channel; // APRS Report Channel: 0x00=1, ... 0x07=8 uint8_t _unused55[9]; // 0 } channel_t; // // General settings: 0x640 bytes at 0x02500000. // typedef struct { // Bytes 0-5. uint8_t _unused0[6]; // Bytes 6-7. uint8_t power_on; // Power-on Interface #define PWON_DEFAULT 0 // Default #define PWON_CUST_CHAR 1 // Custom Char #define PWON_CUST_PICT 2 // Custom Picture uint8_t _unused7; // Bytes 8-0x5ff. uint8_t _unused8[0x5f8]; // Bytes 0x600-0x61f uint8_t intro_line1[16]; // Up to 14 characters uint8_t intro_line2[16]; // Up to 14 characters // Bytes 0x620-0x63f uint8_t password[16]; // Up to 8 ascii digits uint8_t _unused630[16]; // 0xff } general_settings_t; // // Radio ID table: 250 entries, 0x1f40 bytes at 0x02580000. // typedef struct { // Bytes 0-3. uint8_t id[4]; // Up to 8 BCD digits #define GET_ID(x) (((x)[0] >> 4) * 10000000 +\ ((x)[0] & 15) * 1000000 +\ ((x)[1] >> 4) * 100000 +\ ((x)[1] & 15) * 10000 +\ ((x)[2] >> 4) * 1000 +\ ((x)[2] & 15) * 100 +\ ((x)[3] >> 4) * 10 +\ ((x)[3] & 15)) // Byte 4. uint8_t _unused4; // 0 // Bytes 5-20 uint8_t name[16]; // Name // Bytes 21-31 uint8_t _unused21[11]; // 0 } radioid_t; // // Contact data: 100 bytes per record. // typedef struct { // Byte 0 uint8_t type; // Call Type: Group Call, Private Call or All Call #define CALL_PRIVATE 0 #define CALL_GROUP 1 #define CALL_ALL 2 // Bytes 1-16 uint8_t name[16]; // Contact Name (ASCII) // Bytes 17-34 uint8_t _unused17[18]; // 0 // Bytes 35-38 uint8_t id[4]; // Call ID: BCD coded 8 digits #define CONTACT_ID(ct) GET_ID((ct)->id) // Byte 39 uint8_t call_alert; // Call Alert: None, Ring, Online Alert #define ALERT_NONE 0 #define ALERT_RING 1 #define ALERT_ONLINE 2 // Bytes 40-99 uint8_t _unused40[60]; // 0 } contact_t; // // Group list data. // typedef struct { // Bytes 0-255 uint32_t member[64]; // Contacts: 0=Contact1, 0xffffffff=Empty // Bytes 256-319 uint8_t name[35]; // Group List Name (ASCII) uint8_t unused[29]; // 0 } grouplist_t; // // Scan list data: 192 bytes. // typedef struct { // Bytes 0-1 uint8_t _unused0; // 0 uint8_t prio_ch_select; // Priority Channel Select #define PRIO_CHAN_OFF 0 // Off #define PRIO_CHAN_SEL1 1 // Priority Channel Select1 #define PRIO_CHAN_SEL2 2 // Priority Channel Select2 #define PRIO_CHAN_SEL12 3 // Priority Channel Select1 + Priority Channel Select2 // Bytes 2-5 uint16_t priority_ch1; // Priority Channel 1: 0=Current Channel, 0xffff=Off uint16_t priority_ch2; // Priority Channel 2: 0=Current Channel, 0xffff=Off // Bytes 6-13 uint16_t look_back_a; // Look Back Time A, sec*10 uint16_t look_back_b; // Look Back Time B, sec*10 uint16_t dropout_delay; // Dropout Delay Time, sec*10 uint16_t dwell; // Dwell Time, sec*10 // Byte 14 uint8_t revert_channel; // Revert Channel #define REVCH_SELECTED 0 // Selected #define REVCH_SEL_TB 1 // Selected + TalkBack #define REVCH_PRIO_CH1 2 // Priority Channel Select1 #define REVCH_PRIO_CH2 3 // Priority Channel Select2 #define REVCH_LAST_CALLED 4 // Last Called #define REVCH_LAST_USED 5 // Last Used #define REVCH_PRIO_CH1_TB 6 // Priority Channel Select1 + TalkBack #define REVCH_PRIO_CH2_TB 7 // Priority Channel Select2 + TalkBack // Bytes 15-31 uint8_t name[16]; // Scan List Name (ASCII) uint8_t _unused31; // 0 // Bytes 32-131 uint16_t member[50]; // Channels, 0xffff=empty // Bytes 132-191 uint8_t _unused132[60]; // 0 } scanlist_t; // // Entry of callsign map: 8 bytes. // typedef struct { uint32_t id; // DMR ID uint32_t offset; // Offset in the callsign data blob } callsign_map_t; // // Sizes of callsign database. // typedef struct { uint32_t count; // Number of records uint32_t last; // Last address of data blob uint32_t _unused3; uint32_t _unused4; } callsign_sizes_t; static const char *POWER_NAME[] = { "Low", "Mid", "High", "Turbo" }; static const char *DIGITAL_ADMIT_NAME[] = { "-", "Free", "NColor", "Color" }; static const char *ANALOG_ADMIT_NAME[] = { "-", "Free", "Tone", "Tone" }; static const char *BANDWIDTH[] = { "12.5", "25" }; static const char *CONTACT_TYPE[] = { "Private", "Group", "All", "Unknown" }; static const char *ALERT_TYPE[] = { "-", "+", "Online", "Unknown" }; // // CTCSS tones, Hz*10. // #define NCTCSS 51 static const int CTCSS_TONES[NCTCSS] = { 625, 670, 693, 719, 744, 770, 797, 825, 854, 885, 915, 948, 974, 1000, 1035, 1072, 1109, 1148, 1188, 1230, 1273, 1318, 1365, 1413, 1462, 1514, 1567, 1598, 1622, 1655, 1679, 1713, 1738, 1773, 1799, 1835, 1862, 1899, 1928, 1966, 1995, 2035, 2065, 2107, 2181, 2257, 2291, 2336, 2418, 2503, 2541, }; // // Print a generic information about the device. // static void d868uv_print_version(radio_device_t *radio, FILE *out) { // Empty. } // // Return true when the specified region has to be skipped. // Skip unused channels, contacts, zones and scanlists. // static int skip_region(unsigned addr, unsigned file_offset, uint8_t *mem, unsigned nbytes) { int index; // Channels. if (addr >= 0x00800000 && addr < 0x01000000) { index = (file_offset - OFFSET_BANK1) / 64; if (index < NCHAN) { uint8_t *bitmap = &radio_mem[OFFSET_CHAN_MAP]; if ((bitmap[index / 8] >> (index & 7)) & 1) { // Channel is valid, don't skip. return 0; } // Invalid channel: skip it, erase data. if (mem) { memset(mem, 0xff, nbytes); } return 1; } } // Contacts. if (addr >= 0x02680000 && addr < 0x02900000) { index = (file_offset - OFFSET_CONTACTS) / 100; if (index < NCONTACTS) { uint8_t *cmap = GET_CONTACT_MAP(); if ((cmap[index / 8] >> (index & 7)) & 1) { // Invalid contact: skip it, erase data. if (mem) { memset(mem, 0xff, nbytes); } return 1; } // Contact is valid, don't skip. return 0; } } // Zones. if (addr >= 0x01000000 && addr < 0x01080000) { index = (file_offset - OFFSET_ZONELISTS) / 512; if (index < NZONES) { uint8_t *zmap = GET_ZONEMAP(); if ((zmap[index / 8] >> (index & 7)) & 1) { // Zone is valid, don't skip. return 0; } // Invalid zone: skip it, erase data. if (mem) { memset(mem, 0xff, nbytes); } return 1; } } // Scanlists. if (addr >= 0x01080000 && addr < 0x01640000) { index = (file_offset - OFFSET_SCANLISTS) / 192; if (index < NSCANL) { uint8_t *slmap = GET_SCANL_MAP(); if ((slmap[index / 8] >> (index & 7)) & 1) { // Scanlist is valid, don't skip. return 0; } // Invalid scanlist: skip it, erase data. if (mem) { memset(mem, 0xff, nbytes); } return 1; } } return 0; } // // Read memory image from the device. // static void d868uv_download(radio_device_t *radio) { fragment_t *f; // Read bitmaps first. for (f=region_map; f->length; f++) { if (f->offset != 0) { serial_read_region(f->address, &radio_mem[f->offset], f->length); } } // Read other regions sequentially. unsigned file_offset = 0; unsigned bytes_transferred = 0; unsigned last_printed = 0; //printf("Address Offset\n"); for (f=region_map; f->length; f++) { unsigned addr = f->address; unsigned nbytes = f->length; //printf("%08x %06x\n", addr, file_offset); while (nbytes > 0) { unsigned n = (nbytes > 64) ? 64 : nbytes; if (! skip_region(addr, file_offset, &radio_mem[file_offset], n)) { if (f->offset == 0) serial_read_region(addr, &radio_mem[file_offset], n); bytes_transferred += n; } file_offset += n; addr += n; nbytes -= n; if (bytes_transferred / (32*1024) != last_printed) { fprintf(stderr, "#"); fflush(stderr); last_printed = bytes_transferred / (32*1024); } } } if (file_offset != MEMSZ) { fprintf(stderr, "\nWrong MEMSZ=%u for D868UV!\n", MEMSZ); fprintf(stderr, "Should be %u; check d868uv-map.h!\n", file_offset); exit(-1); } } // // Get contact by index. // static contact_t *get_contact(int i) { uint8_t *cmap = GET_CONTACT_MAP(); if ((cmap[i / 8] >> (i & 7)) & 1) return 0; return GET_CONTACT(i); } // // Write memory image to the device. // static void d868uv_upload(radio_device_t *radio, int cont_flag) { fragment_t *f; unsigned file_offset = 0; unsigned bytes_transferred = 0; unsigned last_printed = 0; for (f=region_map; f->length; f++) { unsigned addr = f->address; unsigned nbytes = f->length; while (nbytes > 0) { unsigned n = (nbytes > 64) ? 64 : nbytes; if (! skip_region(addr, file_offset, 0, 0)) { serial_write_region(addr, &radio_mem[file_offset], n); bytes_transferred += n; } file_offset += n; addr += n; nbytes -= n; if (bytes_transferred / (32*1024) != last_printed) { fprintf(stderr, "#"); fflush(stderr); last_printed = bytes_transferred / (32*1024); } } } if (file_offset != MEMSZ) { fprintf(stderr, "\nWrong MEMSZ=%u for D868UV!\n", MEMSZ); fprintf(stderr, "Should be %u; check d868uv-map.h!\n", file_offset); exit(-1); } // // Build and upload a map of IDs to contacts. // The map has to be sorted by ID. // uint64_t map[8*NCONTACTS + 72]; int index, ncontacts = 0; memset(map, 0xff, sizeof(map)); for (index=0; indexid[0] << 25 | ct->id[1] << 17 | ct->id[2] << 9 | ct->id[3] << 1; if (ct->type == CALL_GROUP) item |= 1; item |= (uint64_t) index << 32; int k; for (k=0; k (uint32_t)item) { // Insert item there and shift the rest. uint64_t prev = map[k]; map[k] = item; item = prev; } } } //printf("\n"); //print_hex((uint8_t*)map, ncontacts*8 + 8); //printf("\n"); serial_write_region(ADDR_CONT_ID_LIST, (uint8_t*)map, (ncontacts*8 + 8 + 63) / 64 * 64); } // // Check whether the memory image is compatible with this device. // static int d868uv_is_compatible(radio_device_t *radio) { if (memcmp("D868UVE", (char*)&radio_mem[0], 7) == 0) return 1; if (memcmp("D878UV", (char*)&radio_mem[0], 6) == 0) return 1; if (memcmp("D6X2UV", (char*)&radio_mem[0], 6) == 0) return 1; return 0; } static void print_id(FILE *out, int verbose) { radioid_t *ri = GET_RADIOID(); unsigned id = GET_ID(ri->id); if (verbose) fprintf(out, "\n# Unique DMR ID and name of this radio."); fprintf(out, "\nID: %u\nName: ", id); if (VALID_TEXT(ri->name)) { print_ascii(out, ri->name, 16, 0); } else { fprintf(out, "-"); } fprintf(out, "\n"); } static void print_intro(FILE *out, int verbose) { general_settings_t *gs = GET_SETTINGS(); if (verbose) fprintf(out, "\n# Text displayed when the radio powers up.\n"); fprintf(out, "Intro Line 1: "); if (VALID_TEXT(gs->intro_line1)) { print_ascii(out, gs->intro_line1, 14, 0); } else { fprintf(out, "-"); } fprintf(out, "\nIntro Line 2: "); if (VALID_TEXT(gs->intro_line2)) { print_ascii(out, gs->intro_line2, 14, 0); } else { fprintf(out, "-"); } fprintf(out, "\n"); } // // Get channel bank by index. // static channel_t *get_bank(int i) { return (channel_t*) &radio_mem[OFFSET_BANK1 + i*0x2000]; } // // Get channel by index. // static channel_t *get_channel(int i) { channel_t *bank = get_bank(i >> 7); uint8_t *bitmap = &radio_mem[OFFSET_CHAN_MAP]; if ((bitmap[i / 8] >> (i & 7)) & 1) return &bank[i % 128]; else return 0; } // // Do we have any channels of given mode? // static int have_channels(int mode) { int i; for (i=0; ichannel_mode == mode) return 1; // Treat D+A mode as digital. if (mode == MODE_DIGITAL && ch->channel_mode == MODE_D_A) return 1; // Treat A+D mode as analog. if (mode == MODE_ANALOG && ch->channel_mode == MODE_A_D) return 1; } return 0; } // // Return true when any contacts are present. // static int have_contacts() { uint8_t *cmap = GET_CONTACT_MAP(); int i; for (i=0; i<(NCONTACTS+7)/8; i++) { if (cmap[i] != 0xff) return 1; } return 0; } // // Print frequency (BCD value). // static void print_rx_freq(FILE *out, unsigned data) { fprintf(out, "%d%d%d.%d%d%d", (data >> 4) & 15, data & 15, (data >> 12) & 15, (data >> 8) & 15, (data >> 20) & 15, (data >> 16) & 15); if (((data >> 24) & 0xff) == 0) { fputs(" ", out); } else { fprintf(out, "%d", (data >> 28) & 15); if (((data >> 24) & 15) == 0) { fputs(" ", out); } else { fprintf(out, "%d", (data >> 24) & 15); } } } // // Convert a 4-byte frequency value from binary coded decimal // to integer format (in Hertz). // static int bcd_to_hz(unsigned bcd) { int a = (bcd >> 4) & 15; int b = bcd & 15; int c = (bcd >> 12) & 15; int d = (bcd >> 8) & 15; int e = (bcd >> 20) & 15; int f = (bcd >> 16) & 15; int g = (bcd >> 28) & 15; int h = (bcd >> 24) & 15; return (((((((a*10 + b) * 10 + c) * 10 + d) * 10 + e) * 10 + f) * 10 + g) * 10 + h) * 10; } // // Print the transmit offset or frequency. // TX value is a delta. // static void print_tx_offset(FILE *out, unsigned tx_offset_bcd, unsigned mode) { int offset; switch (mode) { default: case RM_SIMPLEX: // TX frequency = RX frequency fprintf(out, "+0 "); break; case RM_TXPOS: // Positive TX offset offset = bcd_to_hz(tx_offset_bcd); fprintf(out, "+"); print_mhz(out, offset); break; case RM_TXNEG: // Negative TX offset offset = bcd_to_hz(tx_offset_bcd); fprintf(out, "-"); print_mhz(out, offset); break; } } // // Return scan list index for specified channel. // It depends on radio type. // static int get_scanlist_index(radio_device_t *radio, channel_t *ch) { if (radio == &radio_dmr6x2) { // Radio DMR-6x2 has eight scan lists per channel. return ch->aprs_channel; } else { return ch->scan_list_index; } } // // Print base parameters of the channel: // Name // RX Frequency // TX Frequency // Power // Scan List // TOT // RX Only // static void print_chan_base(FILE *out, radio_device_t *radio, channel_t *ch, int cnum) { fprintf(out, "%5d ", cnum); print_ascii(out, ch->name, 16, 1); fprintf(out, " "); print_rx_freq(out, ch->rx_frequency); fprintf(out, " "); print_tx_offset(out, ch->tx_offset, ch->repeater_mode); fprintf(out, "%-5s ", POWER_NAME[ch->power]); int scanlist_index = get_scanlist_index(radio, ch); if (scanlist_index == 0xff) fprintf(out, "- "); else fprintf(out, "%-4d ", scanlist_index + 1); // Transmit timeout timer on D868UV is configured globally, // not per channel. So we don't print it here. fprintf(out, "- "); fprintf(out, "%c ", "-+"[ch->rx_only]); } static void print_digital_channels(FILE *out, radio_device_t *radio, int verbose) { int i; if (verbose) { fprintf(out, "# Table of digital channels.\n"); fprintf(out, "# 1) Channel number: 1-%d\n", NCHAN); fprintf(out, "# 2) Name: up to 16 characters, use '_' instead of space\n"); fprintf(out, "# 3) Receive frequency in MHz\n"); fprintf(out, "# 4) Transmit frequency or +/- offset in MHz\n"); fprintf(out, "# 5) Transmit power: High, Mid, Low, Turbo\n"); fprintf(out, "# 6) Scan list: - or index in Scanlist table\n"); fprintf(out, "# 7) Transmit timeout timer: (unused)\n"); fprintf(out, "# 8) Receive only: -, +\n"); fprintf(out, "# 9) Admit criteria: -, Free, Color, NColor\n"); fprintf(out, "# 10) Color code: 0, 1, 2, 3... 15\n"); fprintf(out, "# 11) Time slot: 1 or 2\n"); fprintf(out, "# 12) Receive group list: - or index in Grouplist table\n"); fprintf(out, "# 13) Contact for transmit: - or index in Contacts table\n"); fprintf(out, "#\n"); } fprintf(out, "Digital Name Receive Transmit Power Scan TOT RO Admit Color Slot RxGL TxContact"); fprintf(out, "\n"); for (i=0; ichannel_mode != MODE_DIGITAL && ch->channel_mode != MODE_D_A) { // Select digital channels continue; } print_chan_base(out, radio, ch, i+1); // Print digital parameters of the channel: // Admit Criteria // Color Code // Repeater Slot // Group List // Contact Name fprintf(out, "%-6s ", DIGITAL_ADMIT_NAME[ch->tx_permit]); fprintf(out, "%-5d %-3d ", ch->color_code, 1 + ch->slot2); if (ch->group_list_index == 0xff) fprintf(out, "- "); else fprintf(out, "%-4d ", ch->group_list_index + 1); if (ch->contact_index == 0xffff) fprintf(out, "-"); else fprintf(out, "%-4d", ch->contact_index + 1); // Print contact name as a comment. if (ch->contact_index != 0xffff) { contact_t *ct = get_contact(ch->contact_index); if (ct) { fprintf(out, " # "); print_ascii(out, ct->name, 16, 0); } } fprintf(out, "\n"); } } // // Print CTSS tone. // static void print_ctcss(FILE *out, unsigned index, unsigned custom) { int dhz = (index < NCTCSS) ? CTCSS_TONES[index] : custom; unsigned a = dhz / 1000; unsigned b = (dhz / 100) % 10; unsigned c = (dhz / 10) % 10; unsigned d = dhz % 10; if (a == 0) fprintf(out, "%d%d.%d ", b, c, d); else fprintf(out, "%d%d%d.%d", a, b, c, d); } // // Print DCS tone. // static void print_dcs(FILE *out, unsigned dcs) { unsigned i = (dcs >> 9) & 1; unsigned a = (dcs >> 6) & 7; unsigned b = (dcs >> 3) & 7; unsigned c = dcs & 7; fprintf(out, "D%d%d%d%c", a, b, c, i ? 'I' : 'N'); } static void print_analog_channels(FILE *out, radio_device_t *radio, int verbose) { int i; if (verbose) { fprintf(out, "# Table of analog channels.\n"); fprintf(out, "# 1) Channel number: 1-%d\n", NCHAN); fprintf(out, "# 2) Name: up to 16 characters, use '_' instead of space\n"); fprintf(out, "# 3) Receive frequency in MHz\n"); fprintf(out, "# 4) Transmit frequency or +/- offset in MHz\n"); fprintf(out, "# 5) Transmit power: High, Mid, Low, Turbo\n"); fprintf(out, "# 6) Scan list: - or index\n"); fprintf(out, "# 7) Transmit timeout timer: (unused)\n"); fprintf(out, "# 8) Receive only: -, +\n"); fprintf(out, "# 9) Admit criteria: -, Free, Tone\n"); fprintf(out, "# 10) Squelch level: Normal (unused)\n"); fprintf(out, "# 11) Guard tone for receive, or '-' to disable\n"); fprintf(out, "# 12) Guard tone for transmit, or '-' to disable\n"); fprintf(out, "# 13) Bandwidth in kHz: 12.5, 25\n"); fprintf(out, "#\n"); } fprintf(out, "Analog Name Receive Transmit Power Scan TOT RO Admit Squelch RxTone TxTone Width"); fprintf(out, "\n"); for (i=0; ichannel_mode != MODE_ANALOG && ch->channel_mode != MODE_A_D) { // Select analog channels continue; } print_chan_base(out, radio, ch, i+1); // Print analog parameters of the channel: // Admit Criteria // Squelch // CTCSS/DCS Dec // CTCSS/DCS Enc // Bandwidth fprintf(out, "%-6s ", ANALOG_ADMIT_NAME[ch->tx_permit]); fprintf(out, "%-7s ", "Normal"); if (ch->rx_ctcss) print_ctcss(out, ch->ctcss_receive, ch->custom_ctcss); else if (ch->rx_dcs) print_dcs(out, ch->dcs_receive); else fprintf(out, "- "); fprintf(out, " "); if (ch->tx_ctcss) print_ctcss(out, ch->ctcss_transmit, ch->custom_ctcss); else if (ch->tx_dcs) print_dcs(out, ch->dcs_transmit); else fprintf(out, "- "); fprintf(out, " %s", BANDWIDTH[ch->bandwidth]); fprintf(out, "\n"); } } // // Return true when any zones are present. // static int have_zones() { uint8_t *zmap = GET_ZONEMAP(); int i; for (i=0; i<(NZONES+7)/8; i++) { if (zmap[i] != 0) return 1; } return 0; } // // Return true when any scanlists are present. // static int have_scanlists() { uint8_t *slmap = GET_SCANL_MAP(); int i; for (i=0; i<(NSCANL+7)/8; i++) { if (slmap[i] != 0) return 1; } return 0; } // // Find a zone with given index. // Return false when zone is not valid. // Set zname and zlist to a zone name and member list. // static int get_zone(int i, uint8_t **zname, uint16_t **zlist) { uint8_t *zmap = GET_ZONEMAP(); if ((zmap[i / 8] >> (i & 7)) & 1) { // Zone is valid. *zname = GET_ZONENAME(i); *zlist = GET_ZONELIST(i); return 1; } else { return 0; } } // // Get scanlist by index. // static scanlist_t *get_scanlist(int i) { uint8_t *slmap = GET_SCANL_MAP(); if ((slmap[i / 8] >> (i & 7)) & 1) return GET_SCANLIST(i); return 0; } static void print_chanlist16(FILE *out, uint16_t *unsorted, int nchan) { int last = -1; int range = 0; int n; uint16_t data[nchan]; // Sort the list before printing. memcpy(data, unsorted, nchan * sizeof(uint16_t)); qsort(data, nchan, sizeof(uint16_t), compare_index_ffff); for (n=0; n 0) fprintf(out, ","); fprintf(out, "%d", cnum); } last = cnum; } if (range) fprintf(out, "-%d", last); } static void print_chanlist32(FILE *out, uint32_t *unsorted, int nchan) { int last = -1; int range = 0; int n; uint32_t data[nchan]; // Sort the list before printing. memcpy(data, unsorted, nchan * sizeof(uint32_t)); qsort(data, nchan, sizeof(uint32_t), compare_index_ffffffff); for (n=0; n 0) fprintf(out, ","); fprintf(out, "%d", cnum); } last = cnum; } if (range) fprintf(out, "-%d", last); } static int have_grouplists() { int i; for (i=0; iname); if (verbose) d868uv_print_version(radio, out); // // Channels. // if (have_channels(MODE_DIGITAL)) { fprintf(out, "\n"); print_digital_channels(out, radio, verbose); } if (have_channels(MODE_ANALOG)) { fprintf(out, "\n"); print_analog_channels(out, radio, verbose); } // // Zones. // if (have_zones()) { fprintf(out, "\n"); if (verbose) { fprintf(out, "# Table of channel zones.\n"); fprintf(out, "# 1) Zone number: 1-%d\n", NZONES); fprintf(out, "# 2) Name: up to 16 characters, use '_' instead of space\n"); fprintf(out, "# 3) List of channels: numbers and ranges (N-M) separated by comma\n"); fprintf(out, "#\n"); } fprintf(out, "Zone Name Channels\n"); for (i=0; iname, 16, 1); if ((sl->prio_ch_select == PRIO_CHAN_SEL1 || sl->prio_ch_select == PRIO_CHAN_SEL12) && sl->priority_ch1 != 0xffff) { if (sl->priority_ch1 == 0) { fprintf(out, " Curr "); } else { fprintf(out, " %-4d ", sl->priority_ch1); } } else { fprintf(out, " - "); } if ((sl->prio_ch_select == PRIO_CHAN_SEL2 || sl->prio_ch_select == PRIO_CHAN_SEL12) && sl->priority_ch2 != 0xffff) { if (sl->priority_ch2 == 0) { fprintf(out, "Curr "); } else { fprintf(out, "%-4d ", sl->priority_ch2); } } else { fprintf(out, "- "); } if (sl->revert_channel == REVCH_LAST_CALLED) { fprintf(out, "Last "); } else { fprintf(out, "Sel "); } if (sl->member[0] != 0xffff) { print_chanlist16(out, sl->member, 50); } else { fprintf(out, "-"); } fprintf(out, "\n"); } } // // Contacts. // if (have_contacts()) { fprintf(out, "\n"); if (verbose) { fprintf(out, "# Table of contacts.\n"); fprintf(out, "# 1) Contact number: 1-%d\n", NCONTACTS); fprintf(out, "# 2) Name: up to 16 characters, use '_' instead of space\n"); fprintf(out, "# 3) Call type: Group, Private, All\n"); fprintf(out, "# 4) Call ID: 1...16777215\n"); fprintf(out, "# 5) Incoming call alert: -, +, Online\n"); fprintf(out, "#\n"); } fprintf(out, "Contact Name Type ID RxTone\n"); for (i=0; iname, 16, 1); fprintf(out, " %-7s %-8d %s\n", CONTACT_TYPE[ct->type & 3], CONTACT_ID(ct), ALERT_TYPE[ct->call_alert & 3]); } } // // Group lists. // if (have_grouplists()) { fprintf(out, "\n"); if (verbose) { fprintf(out, "# Table of group lists.\n"); fprintf(out, "# 1) Group list number: 1-%d\n", NGLISTS); fprintf(out, "# 2) Name: up to 35 characters, use '_' instead of space\n"); fprintf(out, "# 3) List of contacts: numbers and ranges (N-M) separated by comma\n"); fprintf(out, "#\n"); } fprintf(out, "Grouplist Name Contacts\n"); for (i=0; iname, 35, 1); fprintf(out, " "); print_chanlist32(out, gl->member, 64); fprintf(out, "\n"); } } // // Text messages. // if (have_messages()) { fprintf(out, "\n"); if (verbose) { fprintf(out, "# Table of text messages.\n"); fprintf(out, "# 1) Message number: 1-%d\n", NMESSAGES); fprintf(out, "# 2) Text: up to 200 characters\n"); fprintf(out, "#\n"); } fprintf(out, "Message Text\n"); for (i=0; iname, value, 16, 0); return; } if (strcasecmp ("ID", param) == 0) { uint32_t id = strtoul(value, 0, 0); ri->id[0] = ((id / 10000000) << 4) | ((id / 1000000) % 10); ri->id[1] = ((id / 100000 % 10) << 4) | ((id / 10000) % 10); ri->id[2] = ((id / 1000 % 10) << 4) | ((id / 100) % 10); ri->id[3] = ((id / 10 % 10) << 4) | (id % 10); return; } general_settings_t *gs = GET_SETTINGS(); if (strcasecmp ("Intro Line 1", param) == 0) { ascii_decode_uppercase(gs->intro_line1, value, 14, 0); gs->power_on = PWON_CUST_CHAR; return; } if (strcasecmp ("Intro Line 2", param) == 0) { ascii_decode_uppercase(gs->intro_line2, value, 14, 0); gs->power_on = PWON_CUST_CHAR; return; } fprintf(stderr, "Unknown parameter: %s = %s\n", param, value); exit(-1); } // // Check that the radio does support this frequency. // static int is_valid_frequency(int mhz) { if (mhz >= 136 && mhz <= 174) return 1; if (mhz >= 400 && mhz <= 480) return 1; return 0; } // // Find CTCSS value in standard table. // Otherwise return NCTCSS. // static int ctcss_index(unsigned value) { int i; for (i=0; i> 7) + (i % 128); uint8_t *bitmap = &radio_mem[OFFSET_CHAN_MAP]; bitmap[i / 8] |= 1 << (i & 7); memset(ch, 0, sizeof(channel_t)); ascii_decode(ch->name, name, 16, 0); ch->rx_frequency = mhz_to_ghefcdab(rx_mhz); if (tx_mhz > rx_mhz) { ch->repeater_mode = RM_TXPOS; ch->tx_offset = mhz_to_ghefcdab(tx_mhz - rx_mhz); } else if (tx_mhz < rx_mhz) { ch->repeater_mode = RM_TXNEG; ch->tx_offset = mhz_to_ghefcdab(rx_mhz - tx_mhz); } else { ch->repeater_mode = RM_SIMPLEX; ch->tx_offset = 0x00000100; } ch->channel_mode = mode; ch->power = power; ch->bandwidth = width; ch->rx_only = rxonly; ch->slot2 = (timeslot == 2); ch->color_code = colorcode; ch->tx_permit = admit; ch->contact_index = contact - 1; ch->group_list_index = grouplist - 1; ch->custom_ctcss = 251.1 * 10; if (radio == &radio_dmr6x2) { // Radio DMR-6x2 has eight scan lists per channel. ch->scan_list_index = 0; // Channel Measure Mode = Off ch->aprs_channel = scanlist - 1; // Scan list 1 memset(ch->_unused55, 0xff, 7); // Scan lists 2-8 = Disable } else { ch->scan_list_index = scanlist - 1; } // rxtone and txtone are positive for DCS and negative for CTCSS. if (rxtone > 0) { // Receive DCS ch->rx_dcs = 1; ch->dcs_receive = rxtone - 1; } else if (rxtone < 0) { // Receive CTCSS ch->rx_ctcss = 1; ch->ctcss_receive = ctcss_index(-rxtone); if (ch->ctcss_receive == NCTCSS) { ch->custom_ctcss = -rxtone; } } if (ch->rx_ctcss == 0 && ch->rx_dcs == 0) { ch->squelch_mode = SQ_CARRIER; } else { ch->squelch_mode = SQ_TONE; } if (txtone > 0) { // Transmit DCS ch->tx_dcs = 1; ch->dcs_transmit = txtone - 1; } else if (txtone < 0) { // Transmit CTCSS ch->tx_ctcss = 1; ch->ctcss_transmit = ctcss_index(-txtone); if (ch->ctcss_transmit == NCTCSS) { ch->custom_ctcss = -txtone; } } } // // Erase all channels. // static void erase_channels() { memset(&radio_mem[OFFSET_BANK1], 0xff, NCHAN * 64); memset(&radio_mem[OFFSET_CHAN_MAP], 0, (NCHAN + 7) / 8); } // // Erase all zones. // static void erase_zones() { int i; for (i=0; i NCHAN) { fprintf(stderr, "Bad channel number.\n"); return 0; } if (sscanf(rxfreq_str, "%lf", &rx_mhz) != 1 || !is_valid_frequency(rx_mhz)) { fprintf(stderr, "Bad receive frequency.\n"); return 0; } if (sscanf(offset_str, "%lf", &tx_mhz) != 1) { badtx: fprintf(stderr, "Bad transmit frequency.\n"); return 0; } if (offset_str[0] == '-' || offset_str[0] == '+') tx_mhz += rx_mhz; if (! is_valid_frequency(tx_mhz)) goto badtx; if (strcasecmp("High", power_str) == 0) { power = POWER_HIGH; } else if (strcasecmp("Low", power_str) == 0) { power = POWER_LOW; } else if (strcasecmp("Mid", power_str) == 0) { power = POWER_MIDDLE; } else if (strcasecmp("Turbo", power_str) == 0) { power = POWER_TURBO; } else { fprintf(stderr, "Bad power level.\n"); return 0; } if (*scanlist_str == '-') { scanlist = 0; } else { scanlist = atoi(scanlist_str); if (scanlist == 0 || scanlist > NSCANL) { fprintf(stderr, "Bad scanlist.\n"); return 0; } } // Ignore TOT. if (*rxonly_str == '-') { rxonly = 0; } else if (*rxonly_str == '+') { rxonly = 1; } else { fprintf(stderr, "Bad receive only flag.\n"); return 0; } if (*admit_str == '-' || strcasecmp("Always", admit_str) == 0) { admit = PERMIT_ALWAYS; } else if (strcasecmp("Free", admit_str) == 0) { admit = PERMIT_CH_FREE; } else if (strcasecmp("Color", admit_str) == 0) { admit = PERMIT_CC_SAME; } else if (strcasecmp("NColor", admit_str) == 0) { admit = PERMIT_CC_DIFF; } else { fprintf(stderr, "Bad admit criteria.\n"); return 0; } colorcode = atoi(colorcode_str); if (colorcode < 0 || colorcode > 15) { fprintf(stderr, "Bad color code.\n"); return 0; } timeslot = atoi(slot_str); if (timeslot < 1 || timeslot > 2) { fprintf(stderr, "Bad timeslot.\n"); return 0; } if (*grouplist_str == '-') { grouplist = 0; } else { grouplist = atoi(grouplist_str); if (grouplist == 0 || grouplist > NGLISTS) { fprintf(stderr, "Bad receive grouplist.\n"); return 0; } } if (*contact_str == '-') { contact = 0; } else { contact = atoi(contact_str); if (contact == 0 || contact > NCONTACTS) { fprintf(stderr, "Bad transmit contact.\n"); return 0; } } if (first_row && radio->channel_count == 0) { // On first entry, erase all channels, zones and scanlists. erase_channels(); erase_zones(); erase_scanlists(); } setup_channel(radio, num-1, MODE_DIGITAL, name_str, rx_mhz, tx_mhz, power, scanlist, rxonly, admit, colorcode, timeslot, grouplist, contact, 0, 0, BW_12_5_KHZ); radio->channel_count++; return 1; } // // Convert tone string to positive for DCS and negative for CTCSS. // On error, return -1. // Four possible formats: // nnn.n - CTCSS frequency // DnnnN - DCS normal // DnnnI - DCS inverted // '-' - Disabled // static int encode_ctcss_dcs(char *str) { int val; if (*str == '-') { // Disabled return 0; } else if (*str == 'D' || *str == 'd') { // // DCS tone // char *e; val = strtoul(++str, &e, 8); if (val < 0 || val > 511) { return -1; } if (*e == 'N' || *e == 'n') { val += 1; } else if (*e == 'I' || *e == 'i') { val += 513; } else { return -1; } } else if (*str >= '0' && *str <= '9') { // // CTCSS tone // float hz; if (sscanf(str, "%f", &hz) != 1) return -1; // Round to integer. val = hz * 10.0 + 0.5; val = -val; } else { return -1; } return val; } // // Parse one line of Analog channel table. // Start_flag is 1 for the first table row. // Return 0 on failure. // static int parse_analog_channel(radio_device_t *radio, int first_row, char *line) { char num_str[256], name_str[256], rxfreq_str[256], offset_str[256]; char power_str[256], scanlist_str[256], squelch_str[256]; char tot_str[256], rxonly_str[256], admit_str[256]; char rxtone_str[256], txtone_str[256], width_str[256]; int num, power, scanlist, rxonly, admit; int rxtone, txtone, width; double rx_mhz, tx_mhz; if (sscanf(line, "%s %s %s %s %s %s %s %s %s %s %s %s %s", num_str, name_str, rxfreq_str, offset_str, power_str, scanlist_str, tot_str, rxonly_str, admit_str, squelch_str, rxtone_str, txtone_str, width_str) != 13) return 0; num = atoi(num_str); if (num < 1 || num > NCHAN) { fprintf(stderr, "Bad channel number.\n"); return 0; } if (sscanf(rxfreq_str, "%lf", &rx_mhz) != 1 || !is_valid_frequency(rx_mhz)) { fprintf(stderr, "Bad receive frequency.\n"); return 0; } if (sscanf(offset_str, "%lf", &tx_mhz) != 1) { badtx: fprintf(stderr, "Bad transmit frequency.\n"); return 0; } if (offset_str[0] == '-' || offset_str[0] == '+') tx_mhz += rx_mhz; if (! is_valid_frequency(tx_mhz)) goto badtx; if (strcasecmp("High", power_str) == 0) { power = POWER_HIGH; } else if (strcasecmp("Low", power_str) == 0) { power = POWER_LOW; } else if (strcasecmp("Mid", power_str) == 0) { power = POWER_MIDDLE; } else if (strcasecmp("Turbo", power_str) == 0) { power = POWER_TURBO; } else { fprintf(stderr, "Bad power level.\n"); return 0; } if (*scanlist_str == '-') { scanlist = 0; } else { scanlist = atoi(scanlist_str); if (scanlist == 0 || scanlist > NSCANL) { fprintf(stderr, "Bad scanlist.\n"); return 0; } } // Ignore TOT. if (*rxonly_str == '-') { rxonly = 0; } else if (*rxonly_str == '+') { rxonly = 1; } else { fprintf(stderr, "Bad receive only flag.\n"); return 0; } if (*admit_str == '-' || strcasecmp("Always", admit_str) == 0) { admit = PERMIT_ALWAYS; } else if (strcasecmp("Free", admit_str) == 0) { // Busy Lock = Repeater admit = PERMIT_CH_FREE; } else if (strcasecmp("Tone", admit_str) == 0) { // Busy Lock = Busy admit = PERMIT_CC_SAME; } else { fprintf(stderr, "Bad admit criteria.\n"); return 0; } // Ignore squelch. rxtone = encode_ctcss_dcs(rxtone_str); if (rxtone == -1) { fprintf(stderr, "Bad receive tone.\n"); return 0; } txtone = encode_ctcss_dcs(txtone_str); if (txtone == -1) { fprintf(stderr, "Bad transmit tone.\n"); return 0; } if (strcasecmp ("12.5", width_str) == 0) { width = BW_12_5_KHZ; } else if (strcasecmp ("25", width_str) == 0) { width = BW_25_KHZ; } else { fprintf (stderr, "Bad width.\n"); return 0; } if (first_row && radio->channel_count == 0) { // On first entry, erase all channels, zones and scanlists. erase_channels(); } setup_channel(radio, num-1, MODE_ANALOG, name_str, rx_mhz, tx_mhz, power, scanlist, rxonly, admit, 0, 1, 0, 0, rxtone, txtone, width); radio->channel_count++; return 1; } // // Set name for a given zone. // static void setup_zone(int index, const char *name) { uint8_t *zmap = GET_ZONEMAP(); zmap[index / 8] |= 1 << (index & 7); ascii_decode(GET_ZONENAME(index), name, 16, 0); } // // Add channel to a zone. // Return 0 on failure. // static int zone_append(int index, int cnum) { uint16_t *zlist = GET_ZONELIST(index); uint16_t *zchan_a = GET_ZONE_CHAN_A(index); uint16_t *zchan_b = GET_ZONE_CHAN_B(index); int i; for (i=0; i<250; i++) { if (zlist[i] == cnum) return 1; if (zlist[i] == 0xffff) { zlist[i] = cnum; if (i == 0) { // Set A and B channels. zchan_a[index] = cnum; zchan_b[index] = cnum; } else if (i == 1) { // Set B channel. zchan_b[index] = cnum; } return 1; } } return 0; } // // Parse one line of Zones table. // Return 0 on failure. // static int parse_zones(int first_row, char *line) { char num_str[256], name_str[256], chan_str[256]; int znum; if (sscanf(line, "%s %s %s", num_str, name_str, chan_str) != 3) return 0; znum = strtoul(num_str, 0, 10); if (znum < 1 || znum > NZONES) { fprintf(stderr, "Bad zone number.\n"); return 0; } if (first_row) { // On first entry, erase the Zones table. erase_zones(); } setup_zone(znum-1, name_str); if (*chan_str != '-') { char *str = chan_str; int nchan = 0; int range = 0; int last = 0; // Parse channel list. for (;;) { char *eptr; int cnum = strtoul(str, &eptr, 10); if (eptr == str) { fprintf(stderr, "Zone %d: wrong channel list '%s'.\n", znum, str); return 0; } if (cnum < 1 || cnum > NCHAN) { fprintf(stderr, "Zone %d: wrong channel number %d.\n", znum, cnum); return 0; } if (range) { // Add range. int c; for (c=last+1; c<=cnum; c++) { if (!zone_append(znum-1, c-1)) { fprintf(stderr, "Zone %d: too many channels.\n", znum); return 0; } nchan++; } } else { // Add single channel. if (!zone_append(znum-1, cnum-1)) { fprintf(stderr, "Zone %d: too many channels.\n", znum); return 0; } nchan++; } if (*eptr == 0) break; if (*eptr != ',' && *eptr != '-') { fprintf(stderr, "Zone %d: wrong channel list '%s'.\n", znum, eptr); return 0; } range = (*eptr == '-'); last = cnum; str = eptr + 1; } } return 1; } // // Set parameters for a given scan list. // static void setup_scanlist(int index, const char *name, int prio1, int prio2, int txchan) { scanlist_t *sl = GET_SCANLIST(index); uint8_t *slmap = GET_SCANL_MAP(); slmap[index / 8] |= 1 << (index & 7); memset(sl, 0, 192); memset(sl->member, 0xff, 100); ascii_decode(sl->name, name, 16, 0); sl->priority_ch1 = prio1; // Priority Channel 1: 0=Current Channel, 0xffff=Off sl->priority_ch2 = prio2; // Priority Channel 2: 0=Current Channel, 0xffff=Off sl->revert_channel = txchan; // Revert Channel: Selected or Last Called if (prio2 != 0xffff) { // Priority Channel Select if (prio1 != 0xffff) { sl->prio_ch_select = PRIO_CHAN_SEL12; } else { sl->prio_ch_select = PRIO_CHAN_SEL2; } } else { if (prio1 != 0xffff) { sl->prio_ch_select = PRIO_CHAN_SEL1; } else { sl->prio_ch_select = PRIO_CHAN_OFF; } } sl->look_back_a = 20; // Look Back Time A: 2.0s sl->look_back_b = 30; // Look Back Time B: 3.0s sl->dropout_delay = 31; // Dropout Delay Time: 3.1s sl->dwell = 31; // Dwell Time: 3.1s } // // Add channel to a zone. // Return 0 on failure. // static int scanlist_append(int index, int cnum) { scanlist_t *sl = GET_SCANLIST(index); int i; for (i=0; i<50; i++) { if (sl->member[i] == cnum-1) return 1; if (sl->member[i] == 0xffff) { sl->member[i] = cnum-1; return 1; } } return 0; } // // Parse one line of Scanlist table. // Return 0 on failure. // static int parse_scanlist(int first_row, char *line) { char num_str[256], name_str[256], prio1_str[256], prio2_str[256]; char tx_str[256], chan_str[256]; int snum, prio1, prio2, txchan; if (sscanf(line, "%s %s %s %s %s %s", num_str, name_str, prio1_str, prio2_str, tx_str, chan_str) != 6) return 0; snum = atoi(num_str); if (snum < 1 || snum > NSCANL) { fprintf(stderr, "Bad scan list number.\n"); return 0; } if (first_row) { // On first entry, erase the Scanlists table. erase_scanlists(); } if (*prio1_str == '-') { prio1 = 0xffff; } else if (strcasecmp("Sel", prio1_str) == 0) { prio1 = 0; } else { prio1 = atoi(prio1_str); if (prio1 < 1 || prio1 > NCHAN) { fprintf(stderr, "Bad priority channel 1.\n"); return 0; } } if (*prio2_str == '-') { prio2 = 0xffff; } else if (strcasecmp("Sel", prio2_str) == 0) { prio2 = 0; } else { prio2 = atoi(prio2_str); if (prio2 < 1 || prio2 > NCHAN) { fprintf(stderr, "Bad priority channel 2.\n"); return 0; } } if (strcasecmp("Last", tx_str) == 0) { txchan = REVCH_LAST_CALLED; } else if (strcasecmp("Sel", tx_str) == 0 || strcasecmp("-", tx_str) == 0) { txchan = REVCH_SELECTED; } else { fprintf(stderr, "Bad transmit channel.\n"); return 0; } setup_scanlist(snum-1, name_str, prio1, prio2, txchan); if (*chan_str != '-') { char *str = chan_str; int nchan = 0; int range = 0; int last = 0; // Parse channel list. for (;;) { char *eptr; int cnum = strtoul(str, &eptr, 10); if (eptr == str) { fprintf(stderr, "Scan list %d: wrong channel list '%s'.\n", snum, str); return 0; } if (cnum < 1 || cnum > NCHAN) { fprintf(stderr, "Scan list %d: wrong channel number %d.\n", snum, cnum); return 0; } if (range) { // Add range. int c; for (c=last+1; c<=cnum; c++) { if (!scanlist_append(snum-1, c)) { fprintf(stderr, "Scan list %d: too many channels.\n", snum); return 0; } nchan++; } } else { // Add single channel. if (!scanlist_append(snum-1, cnum)) { fprintf(stderr, "Scan list %d: too many channels.\n", snum); return 0; } nchan++; } if (*eptr == 0) break; if (*eptr != ',' && *eptr != '-') { fprintf(stderr, "Scan list %d: wrong channel list '%s'.\n", snum, eptr); return 0; } range = (*eptr == '-'); last = cnum; str = eptr + 1; } } return 1; } // // Erase all contacts. // static void erase_contacts() { memset(&radio_mem[OFFSET_CONTACTS], 0xff, NCONTACTS*100); memset(GET_CONTACT_LIST(), 0xff, NCONTACTS*4); memset(GET_CONTACT_MAP(), 0xff, (NCONTACTS + 7) / 8); } static void setup_contact(int index, const char *name, int type, int id, int rxalert) { // Fill contact record. contact_t *ct = GET_CONTACT(index); memset(ct, 0, 100); ascii_decode(ct->name, name, 16, 0); ct->id[0] = ((id / 10000000) << 4) | ((id / 1000000) % 10); ct->id[1] = ((id / 100000 % 10) << 4) | ((id / 10000) % 10); ct->id[2] = ((id / 1000 % 10) << 4) | ((id / 100) % 10); ct->id[3] = ((id / 10 % 10) << 4) | (id % 10); ct->type = type; ct->call_alert = rxalert; // Update contact map. uint8_t *cmap = GET_CONTACT_MAP(); cmap[index / 8] &= ~(1 << (index & 7)); // Append to the contact list. uint32_t *clist = GET_CONTACT_LIST(); int i; for (i=0; i index) { // Insert index there and shift the rest. clist[i] = index; index = item; } } } // // Parse one line of Contacts table. // Return 0 on failure. // static int parse_contact(int first_row, char *line) { char num_str[256], name_str[256], type_str[256], id_str[256], rxalert_str[256]; int cnum, type, id, rxalert; if (sscanf(line, "%s %s %s %s %s", num_str, name_str, type_str, id_str, rxalert_str) != 5) return 0; cnum = atoi(num_str); if (cnum < 1 || cnum > NCONTACTS) { fprintf(stderr, "Bad contact number.\n"); return 0; } if (first_row) { // On first entry, erase the Contacts table. erase_contacts(); } if (strcasecmp("Group", type_str) == 0) { type = CALL_GROUP; } else if (strcasecmp("Private", type_str) == 0) { type = CALL_PRIVATE; } else if (strcasecmp("All", type_str) == 0) { type = CALL_ALL; } else { fprintf(stderr, "Bad call type.\n"); return 0; } id = atoi(id_str); if (id < 1 || id > 0xffffff) { fprintf(stderr, "Bad call ID.\n"); return 0; } if (*rxalert_str == '-' || strcasecmp("No", rxalert_str) == 0) { rxalert = ALERT_NONE; } else if (*rxalert_str == '+' || strcasecmp("Yes", rxalert_str) == 0) { rxalert = ALERT_RING; } else if (strcasecmp("Online", rxalert_str) == 0) { rxalert = ALERT_ONLINE; } else { fprintf(stderr, "Bad receive tone flag.\n"); return 0; } setup_contact(cnum-1, name_str, type, id, rxalert); return 1; } static void setup_grouplist(int index, const char *name) { grouplist_t *gl = GET_GROUPLIST(index); ascii_decode(gl->name, name, 35, 0); memset(gl->unused, 0, sizeof(gl->unused)); } // // Add contact to a grouplist. // Return 0 on failure. // static int grouplist_append(int index, int cnum) { grouplist_t *gl = GET_GROUPLIST(index); int i; for (i=0; i<64; i++) { if (gl->member[i] == cnum-1) return 1; if (gl->member[i] == 0xffffffff) { gl->member[i] = cnum-1; return 1; } } return 0; } // // Parse one line of Grouplist table. // Return 0 on failure. // static int parse_grouplist(int first_row, char *line) { char num_str[256], name_str[256], list_str[256]; int glnum; if (sscanf(line, "%s %s %s", num_str, name_str, list_str) != 3) return 0; glnum = strtoul(num_str, 0, 10); if (glnum < 1 || glnum > NGLISTS) { fprintf(stderr, "Bad group list number.\n"); return 0; } if (first_row) { // On first entry, erase the Grouplists table. memset(&radio_mem[OFFSET_GLISTS], 0xff, NGLISTS*320); } setup_grouplist(glnum-1, name_str); if (*list_str != '-') { char *str = list_str; int range = 0; int last = 0; // Parse contact list. for (;;) { char *eptr; int cnum = strtoul(str, &eptr, 10); if (eptr == str) { fprintf(stderr, "Group list %d: wrong contact list '%s'.\n", glnum, str); return 0; } if (cnum < 1 || cnum > NCONTACTS) { fprintf(stderr, "Group list %d: wrong contact number %d.\n", glnum, cnum); return 0; } if (range) { // Add range. int c; for (c=last+1; c<=cnum; c++) { if (!grouplist_append(glnum-1, c)) { fprintf(stderr, "Group list %d: too many contacts.\n", glnum); return 0; } } } else { // Add single contact. if (!grouplist_append(glnum-1, cnum)) { fprintf(stderr, "Group list %d: too many contacts.\n", glnum); return 0; } } if (*eptr == 0) break; if (*eptr != ',' && *eptr != '-') { fprintf(stderr, "Group list %d: wrong contact list '%s'.\n", glnum, eptr); return 0; } range = (*eptr == '-'); last = cnum; str = eptr + 1; } } return 1; } // // Set text for a given message. // static void setup_message(int index, const char *text) { uint8_t *msg = GET_MESSAGE(index); // Skip spaces and tabs. while (*text == ' ' || *text == '\t') text++; ascii_decode(msg, text, 200, 0); } // // Parse one line of Messages table. // Return 0 on failure. // static int parse_messages(int first_row, char *line) { char *text; int mnum; mnum = strtoul(line, &text, 10); if (text == line || mnum < 1 || mnum > NMESSAGES) { fprintf(stderr, "Bad message number.\n"); return 0; } if (first_row) { // On first entry, erase the Messages table. memset(&radio_mem[OFFSET_MESSAGES], 0xff, NMESSAGES*256); } setup_message(mnum-1, text); return 1; } // // Parse table header. // Return table id, or 0 in case of error. // static int d868uv_parse_header(radio_device_t *radio, char *line) { if (strncasecmp(line, "Digital", 7) == 0) return 'D'; if (strncasecmp(line, "Analog", 6) == 0) return 'A'; if (strncasecmp(line, "Zone", 4) == 0) return 'Z'; if (strncasecmp(line, "Scanlist", 8) == 0) return 'S'; if (strncasecmp(line, "Contact", 7) == 0) return 'C'; if (strncasecmp(line, "Grouplist", 9) == 0) return 'G'; if (strncasecmp(line, "Message", 7) == 0) return 'M'; return 0; } // // Parse one line of table data. // Return 0 on failure. // static int d868uv_parse_row(radio_device_t *radio, int table_id, int first_row, char *line) { switch (table_id) { case 'D': return parse_digital_channel(radio, first_row, line); case 'A': return parse_analog_channel(radio, first_row, line); case 'Z': return parse_zones(first_row, line); case 'S': return parse_scanlist(first_row, line); case 'C': return parse_contact(first_row, line); case 'G': return parse_grouplist(first_row, line); case 'M': return parse_messages(first_row, line); } return 0; } // // Update timestamp. // static void d868uv_update_timestamp(radio_device_t *radio) { // No timestamp. } // // Check that configuration is correct. // Return 0 on error. // static int d868uv_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; iname, 16, 0); fprintf(stderr, "': scanlist %d not found.\n", scanlist_index + 1); nerrors++; } } if (ch->contact_index != 0xffff) { contact_t *ct = get_contact(ch->contact_index); if (!ct) { fprintf(stderr, "Channel %d '", i+1); print_ascii(stderr, ch->name, 16, 0); fprintf(stderr, "': contact %d not found.\n", ch->contact_index + 1); nerrors++; } } if (ch->group_list_index != 0xff) { grouplist_t *gl = GET_GROUPLIST(ch->group_list_index); if (!VALID_GROUPLIST(gl)) { fprintf(stderr, "Channel %d '", i+1); print_ascii(stderr, ch->name, 16, 0); fprintf(stderr, "': grouplist %d not found.\n", ch->group_list_index + 1); nerrors++; } } } // Zones: check references to channels. for (i=0; imember[k]; if (cindex != 0xffff) { channel_t *ch = get_channel(cindex); if (!ch) { fprintf(stderr, "Scanlist %d '", i+1); print_ascii(stderr, sl->name, 16, 0); fprintf(stderr, "': channel %d not found.\n", cindex+1); nerrors++; } } } } // Grouplists: check references to contacts. for (i=0; imember[k]; if (cnum != 0xffffffff) { contact_t *ct = get_contact(cnum); if (!ct) { fprintf(stderr, "Grouplist %d '", i+1); print_ascii(stderr, gl->name, 35, 0); fprintf(stderr, "': contact %d not found.\n", cnum); nerrors++; } } } } // Count contacts. for (i=0; iname); nerrors++; break; } } 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; } // // Read and dump the callsign database. // static void dump_csv(radio_device_t *radio) { callsign_sizes_t sz = {0}; // // Dump sizes. // serial_read_region(ADDR_CALLDB_SIZE, (uint8_t*) &sz, 16); printf("Sizes:\n"); print_hex_addr_data(ADDR_CALLDB_SIZE, (uint8_t*)&sz, sizeof(sz)); printf("\n"); // // Dump callsign map. // unsigned addr = ADDR_CALLDB_LIST; unsigned index; printf("Map:\n"); for (index = 0; index < sz.count; index += 16000) { unsigned n = (sz.count - index) * 8; if (n > 128000) n = 128000; uint8_t map[128000]; serial_read_region(addr, map, n); print_hex_addr_data(addr, map, n); addr += 256*1024; } printf("\n"); // // Dump data. // printf("Data:\n"); addr = ADDR_CALLDB_DATA; for (index = 0; index < 10000000; index += 100000) { int n = sz.last - addr; if (n < 0) break; else if (n > 100000) n = 100000; else n = (n + 15) & ~15; // align uint8_t data[100000]; serial_read_region(addr, data, n); print_hex_addr_data(addr, data, n); addr += 256*1024; } } // // Sorting callback for callsign map. // static int compare_callsign_map(const void *ap, const void *bp) { uint32_t a = *(uint32_t*) ap; uint32_t b = *(uint32_t*) bp; if (a < b) return -1; if (a > b) return 1; return 0; } // // Write CSV file to the callsign database. // // The callsign database consists of three parts: // (1) Map of DMR IDs to data offsets: 8 bytes per record. // 04000000: 02-60-04-02-00-00-00-00-04-60-04-02-35-00-00-00 .`.......`..5... // 04000010: 06-60-04-02-70-00-00-00-08-60-04-02-a7-00-00-00 .`..p....`...... // ^^^^^^^^^^^ ^^^^^^^^^^^ // radio id<<1 offset // // The map is stored in 128000-byte chunks with 256kbyte step: // 04000000-0401f3ff, 04040000-0405f3ff, 04080000-0409f3ff, 040c0000-040df3ff, // 04100000-0411f3ff, 04140000-0415f3ff, 04180000-0419f3ff, 041c0000-... // // Up to 10 chunks in total. Last range is 04240000-0425f3ff. // Max 160000 callsigns. // // (2) Sizes: count of records and last data address. // 044c0000: bf-b7-01-00-bd-f6-34-05-00-00-00-00-00-00-00-00 ......4......... // ^^^^^^^^^^^ ^^^^^^^^^^^ // count last address // // (3) Data records: radio id, name, city, callsign, state, country, remarks. // 04500000: 00-01-02-30-01-00-57-61-79-6e-65-20-45-64-77-61 ...0..Wayne Edwa // 04500010: 72-64-00-54-6f-72-6f-6e-74-6f-00-56-45-33-54-48 rd.Toronto.VE3TH // 04500020: 57-00-4f-6e-74-61-72-69-6f-00-43-61-6e-61-64-61 W.Ontario.Canada // 04500030: 00-44-4d-52-00- .DMR. // // The data are stored in 100000-byte chunks with 256kbyte step: // 04500000-0451869f, 04540000-0455869f, ... 05340000-0535869f and so on. // static void d868uv_write_csv(radio_device_t *radio, FILE *csv) { callsign_map_t map[NCALLSIGNS]; callsign_sizes_t sz = {0}; // Allocate data. char *data = malloc(CALLSIGN_SIZE); if (!data) { fprintf(stderr, "Out of memory!\n"); return; } memset(data, 0, CALLSIGN_SIZE); memset(map, 0xff, sizeof(map)); // // Parse CSV file. // // The file has the following format: // Radio ID,Callsign,Name,City,State,Country,Remarks // 3114542,KK6ABQ,Sergey Vakulenko,Santa Clara,California,United States,DMR // // Need to rearrange the fields like: // Radio ID, Name, City, Callsign, State, Country, Remarks // unsigned nbytes = 0; char *radioid, *callsign, *name, *city, *state, *country, *remarks; if (csv_init(csv) < 0) { free(data); return; } while (csv_read(csv, &radioid, &callsign, &name, &city, &state, &country, &remarks)) { radioid = trim_spaces(radioid, 16); callsign = trim_spaces(callsign, 16); name = trim_spaces(name, 16); city = trim_spaces(city, 15); state = trim_spaces(state, 16); country = trim_spaces(country, 16); remarks = trim_spaces(remarks, 16); //printf("%s,%s,%s,%s,%s,%s,%s\n", radioid, callsign, name, city, state, country, remarks); unsigned id = strtoul(radioid, 0, 10); if (id < 1 || id > 0xffffff) { fprintf(stderr, "Bad id: %d\n", id); fprintf(stderr, "Line: '%s,%s,%s,%s,%s,%s,%s'\n", radioid, callsign, name, city, state, country, remarks); return; } // Eastern egg: when file contains id 1 with callsign 'dump', // read the callsign database from the radio // and save to a file. if (id == 1 && strcmp(callsign, "dump") == 0) { free(data); dump_csv(radio); return; } // Add map record. if (sz.count >= NCALLSIGNS) { fprintf(stderr, "WARNING: Too many callsigns!\n"); fprintf(stderr, "Skipping the rest.\n"); break; } callsign_map_t *m = &map[sz.count]; sz.count++; m->id = ((id / 10 % 10) << 5) | (id % 10) << 1 | ((id / 1000 % 10) << 13) | ((id / 100) % 10) << 9 | ((id / 100000 % 10) << 21) | ((id / 10000) % 10) << 17 | ((id / 10000000) << 29) | ((id / 1000000) % 10) << 25; m->offset = nbytes; // Fill data. char *p = &data[nbytes]; // Radio ID. *p++ = 0; *p++ = ((id / 10000000) << 4) | ((id / 1000000) % 10); *p++ = ((id / 100000 % 10) << 4) | ((id / 10000) % 10); *p++ = ((id / 1000 % 10) << 4) | ((id / 100) % 10); *p++ = ((id / 10 % 10) << 4) | (id % 10); *p++ = 0; // Name, city, callsign, state, country, remarks. strcpy(p, name); p += strlen(p) + 1; strcpy(p, city); p += strlen(p) + 1; strcpy(p, callsign); p += strlen(p) + 1; strcpy(p, state); p += strlen(p) + 1; strcpy(p, country); p += strlen(p) + 1; strcpy(p, remarks); p += strlen(p) + 1; nbytes = p - data; } fprintf(stderr, "Total %d contacts, %d bytes.\n", sz.count, nbytes); sz.last = ADDR_CALLDB_DATA + (nbytes / 100000) * 256*1024 + (nbytes % 100000); // Append extra zeroes and align. nbytes = (nbytes + 63) & ~15; // Sort the map by DMR ID. qsort(map, sz.count, sizeof(map[0]), compare_callsign_map); if (! trace_flag) { fprintf(stderr, "Write: "); fflush(stderr); } // // Write callsign map. // unsigned addr = ADDR_CALLDB_LIST; unsigned index; //#define DUMP_NO_WRITE #ifdef DUMP_NO_WRITE printf("Map:\n"); #endif for (index = 0; index < sz.count; index += 16000) { unsigned n = (sz.count - index) * 8; if (n > 128000) n = 128000; #ifdef DUMP_NO_WRITE // Dump the data, for debugging purposes. print_hex_addr_data(addr, (uint8_t*) &map[index], n); #else serial_write_region(addr, (uint8_t*) &map[index], n); #endif addr += 256*1024; fprintf(stderr, "#"); fflush(stderr); } // // Write sizes. // #ifdef DUMP_NO_WRITE printf("\nSizes:\n"); print_hex_addr_data(ADDR_CALLDB_SIZE, (uint8_t*) &sz, 16); #else serial_write_region(ADDR_CALLDB_SIZE, (uint8_t*) &sz, 16); #endif // // Write data. // addr = ADDR_CALLDB_DATA; #ifdef DUMP_NO_WRITE printf("\nData:\n"); #endif for (index = 0; index < nbytes; index += 100000) { unsigned n = nbytes - index; if (n > 100000) n = 100000; #ifdef DUMP_NO_WRITE print_hex_addr_data(addr, (uint8_t*) &data[index], n); #else serial_write_region(addr, (uint8_t*) &data[index], n); #endif addr += 256*1024; fprintf(stderr, "#"); fflush(stderr); } if (! trace_flag) fprintf(stderr, "# done.\n"); free(data); } // // Anytone AT-D868UV // radio_device_t radio_d868uv = { "Anytone AT-D868UV", d868uv_download, d868uv_upload, d868uv_is_compatible, d868uv_read_image, d868uv_save_image, d868uv_print_version, d868uv_print_config, d868uv_verify_config, d868uv_parse_parameter, d868uv_parse_header, d868uv_parse_row, d868uv_update_timestamp, d868uv_write_csv, }; // // Anytone AT-D878UV // radio_device_t radio_d878uv = { "Anytone AT-D878UV", d868uv_download, d868uv_upload, d868uv_is_compatible, d868uv_read_image, d868uv_save_image, d868uv_print_version, d868uv_print_config, d868uv_verify_config, d868uv_parse_parameter, d868uv_parse_header, d868uv_parse_row, d868uv_update_timestamp, d868uv_write_csv, }; // // BTECH DMR-6x2 // radio_device_t radio_dmr6x2 = { "BTECH DMR-6x2", d868uv_download, d868uv_upload, d868uv_is_compatible, d868uv_read_image, d868uv_save_image, d868uv_print_version, d868uv_print_config, d868uv_verify_config, d868uv_parse_parameter, d868uv_parse_header, d868uv_parse_row, d868uv_update_timestamp, d868uv_write_csv, };