diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28265f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.o +dmrconfig diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f579a23 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +CC = gcc -m32 + +VERSION = 1.0 +CFLAGS = -g -O -Wall -Werror -DVERSION='"$(VERSION)"' +LDFLAGS = + +OBJS = main.o util.o radio.o uv380.o +SRCS = main.c util.c radio.c uv380.c +LIBS = + +# Mac OS X +#CFLAGS += -I/usr/local/opt/gettext/include +#LIBS += -L/usr/local/opt/gettext/lib -lintl + +all: dmrconfig + +dmrconfig: $(OBJS) + $(CC) $(LDFLAGS) -o $@ $(OBJS) $(LIBS) + +clean: + rm -f *~ *.o core dmrconfig + +install: dmrconfig + install -c -s dmrconfig /usr/local/bin/dmrconfig + +dmrconfig.linux: dmrconfig + cp -p $< $@ + strip $@ + +### +main.o: main.c radio.h util.h +radio.o: radio.c radio.h util.h +util.o: util.c util.h +uv380.o: uv380.c radio.h util.h diff --git a/README.md b/README.md new file mode 100644 index 0000000..e93edf7 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ + +DMRconfig is a utility for programming digital radios via USB programming cable. +Supported radios: + + * TYT MD-380 + * TYT MD-UV380 + + +## Usage + +Read codeplug from the radio and save it to file 'device.img', +and text configuration to 'device.conf': + + dmrconfig -r [-v] + +Write codeplug to the radio: + + dmrconfig -w [-v] file.img + +Configure the radio from text file. +Previous codeplug is saved to 'backup.img': + + dmrconfig -c [-v] file.conf + +Show configuration from the codeplug file: + + dmrconfig file.img + +Option -v enables tracing of a serial protocol to the radio. + + +## Sources + +Sources are distributed freely under the terms of Apache 2.0 license. +You can download sources via GIT: + + git clone https://github.com/sergev/dmrconfig + + +To build on Linux or Mac OS X, run: + + make + make install + + +Regards, +Serge Vakulenko +KK6ABQ diff --git a/main.c b/main.c new file mode 100644 index 0000000..1ff04da --- /dev/null +++ b/main.c @@ -0,0 +1,174 @@ +/* + * Configuration Utility for DMR radios. + * + * 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 "radio.h" +#include "util.h" + +const char version[] = VERSION; +const char *copyright; + +extern char *optarg; +extern int optind; + +void usage() +{ + fprintf(stderr, _("DMR Config, Version %s, %s\n"), version, copyright); + fprintf(stderr, _("Usage:\n")); + fprintf(stderr, _(" dmrconfig [-v] -r\n")); + fprintf(stderr, _(" Save device binary image to file 'device.img',\n")); + fprintf(stderr, _(" and text configuration to 'device.conf'.\n")); + fprintf(stderr, _(" dmrconfig -w [-v] file.img\n")); + fprintf(stderr, _(" Write image to device.\n")); + fprintf(stderr, _(" dmrconfig -c [-v] file.conf\n")); + fprintf(stderr, _(" Configure device from text file.\n")); + fprintf(stderr, _(" dmrconfig -c [-v] file.img file.conf\n")); + fprintf(stderr, _(" Apply text configuration to the image.\n")); + fprintf(stderr, _(" dmrconfig file.img\n")); + fprintf(stderr, _(" Display configuration from image file.\n")); + fprintf(stderr, _("Options:\n")); + fprintf(stderr, _(" -w Write image to device.\n")); + fprintf(stderr, _(" -c Configure device from text file.\n")); + fprintf(stderr, _(" -v Trace serial protocol.\n")); + exit(-1); +} + +int main(int argc, char **argv) +{ + int write_flag = 0, config_flag = 0; + const char *type = 0; + + // Set locale and message catalogs. + setlocale(LC_ALL, ""); +#ifdef MINGW32 + // Files with localized messages should be placed in + // in c:/Program Files/dmrconfig/ directory. + bindtextdomain("dmrconfig", "c:/Program Files/dmrconfig"); +#else + bindtextdomain("dmrconfig", "/usr/local/share/locale"); +#endif + textdomain("dmrconfig"); + + copyright = _("Copyright (C) 2018 Serge Vakulenko KK6ABQ"); + serial_verbose = 0; + for (;;) { + switch (getopt(argc, argv, "vcwt:")) { + case 'v': ++serial_verbose; continue; + case 'w': ++write_flag; continue; + case 'c': ++config_flag; continue; + case 't': type = optarg; continue; + default: + usage(); + case EOF: + break; + } + break; + } + argc -= optind; + argv += optind; + if (write_flag + config_flag > 1) { + fprintf(stderr, "Only one of -w or -c options is allowed.\n"); + usage(); + } + setvbuf(stdout, 0, _IOLBF, 0); + setvbuf(stderr, 0, _IOLBF, 0); + + if (write_flag) { + // Restore image file to device. + if (argc != 2 || !type) + usage(); + + radio_connect(argv[0], type); + radio_read_image(argv[1]); + radio_print_version(stdout); + radio_upload(0); + radio_disconnect(); + + } else if (config_flag) { + if (argc != 2) + usage(); + + if (is_file(argv[0])) { + // Apply text config to image file. + radio_read_image(argv[0]); + radio_print_version(stdout); + radio_parse_config(argv[1]); + radio_save_image("device.img"); + + } else { + if (!type) + usage(); + + // Update device from text config file. + radio_connect(argv[0], type); + radio_download(); + radio_print_version(stdout); + radio_save_image("backup.img"); + radio_parse_config(argv[1]); + radio_upload(1); + radio_disconnect(); + } + + } else { + if (argc != 1) + usage(); + + if (is_file(argv[0])) { + // Print configuration from image file. + // Load image from file. + radio_read_image(argv[0]); + radio_print_version(stdout); + radio_print_config(stdout, ! isatty(1)); + + } else { + if (!type) + usage(); + + // Dump device to image file. + radio_connect(argv[0], type); + radio_download(); + radio_print_version(stdout); + radio_disconnect(); + radio_save_image("device.img"); + + // Print configuration to file. + const char *filename = "device.conf"; + printf("Print configuration to file '%s'.\n", filename); + FILE *conf = fopen(filename, "w"); + if (! conf) { + perror(filename); + exit(-1); + } + radio_print_version(conf); + radio_print_config(conf, 1); + fclose(conf); + } + } + return 0; +} diff --git a/radio.c b/radio.c new file mode 100644 index 0000000..7e30734 --- /dev/null +++ b/radio.c @@ -0,0 +1,263 @@ +/* + * Configuration Utility for DMR radios. + * + * 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 +#include "radio.h" +#include "util.h" + +int radio_port; // File descriptor of programming serial port +unsigned char radio_mem [1024*1024]; // Radio memory contents, up to 1Mbyte +int radio_progress; // Read/write progress counter + +static radio_device_t *device; // Device-dependent interface + +// +// Close the serial port. +// +void radio_disconnect() +{ + fprintf(stderr, "Close device.\n"); + + // Restore the port mode. + serial_close(radio_port); + + // Radio needs a timeout to reset to a normal state. + mdelay(2000); +} + +// +// Print a generic information about the device. +// +void radio_print_version(FILE *out) +{ + device->print_version(out); +} + +// +// Connect to the radio and identify the type of device. +// +void radio_connect() +{ + //fprintf(stderr, "Connect to %s.\n", port_name); + //radio_port = serial_open(port_name); + //TODO +} + +// +// Read firmware image from the device. +// +void radio_download() +{ + radio_progress = 0; + if (! serial_verbose) + fprintf(stderr, "Read device: "); + + device->download(); + + if (! serial_verbose) + fprintf(stderr, " done.\n"); +} + +// +// Write firmware image to the device. +// +void radio_upload(int cont_flag) +{ + // Check for compatibility. + if (! device->is_compatible()) { + fprintf(stderr, "Incompatible image - cannot upload.\n"); + exit(-1); + } + radio_progress = 0; + if (! serial_verbose) + fprintf(stderr, "Write device: "); + + device->upload(cont_flag); + + if (! serial_verbose) + fprintf(stderr, " done.\n"); +} + +// +// Read firmware image from the binary file. +// +void radio_read_image(char *filename) +{ + FILE *img; + struct stat st; + + fprintf(stderr, "Read image from file '%s'.\n", filename); + + // Guess device type by file size. + if (stat(filename, &st) < 0) { + perror(filename); + exit(-1); + } + switch (st.st_size) { + case 851968: + device = &radio_uv380; + break; + default: + fprintf(stderr, "%s: Unrecognized file size %u bytes.\n", + filename, (int) st.st_size); + exit(-1); + } + + img = fopen(filename, "rb"); + if (! img) { + perror(filename); + exit(-1); + } + device->read_image(img); + fclose(img); +} + +// +// Save firmware image to the binary file. +// +void radio_save_image(char *filename) +{ + FILE *img; + + fprintf(stderr, "Write image to file '%s'.\n", filename); + img = fopen(filename, "w"); + if (! img) { + perror(filename); + exit(-1); + } + device->save_image(img); + fclose(img); +} + +// +// Read the configuration from text file, and modify the firmware. +// +void radio_parse_config(char *filename) +{ + FILE *conf; + char line [256], *p, *v; + int table_id = 0, table_dirty = 0; + + fprintf(stderr, "Read configuration from file '%s'.\n", filename); + conf = fopen(filename, "r"); + if (! conf) { + perror(filename); + exit(-1); + } + + while (fgets(line, sizeof(line), conf)) { + line[sizeof(line)-1] = 0; + + // Strip comments. + v = strchr(line, '#'); + if (v) + *v = 0; + + // Strip trailing spaces and newline. + v = line + strlen(line) - 1; + while (v >= line && (*v=='\n' || *v=='\r' || *v==' ' || *v=='\t')) + *v-- = 0; + + // Ignore comments and empty lines. + p = line; + if (*p == 0) + continue; + + if (*p != ' ') { + // Table finished. + table_id = 0; + + // Find the value. + v = strchr(p, ':'); + if (! v) { + // Table header: get table type. + table_id = device->parse_header(p); + if (! table_id) { +badline: fprintf(stderr, "Invalid line: '%s'\n", line); + exit(-1); + } + table_dirty = 0; + continue; + } + + // Parameter. + *v++ = 0; + + // Skip spaces. + while (*v == ' ' || *v == '\t') + v++; + + device->parse_parameter(p, v); + + } else { + // Table row or comment. + // Skip spaces. + // Ignore comments and empty lines. + while (*p == ' ' || *p == '\t') + p++; + if (*p == '#' || *p == 0) + continue; + if (! table_id) { + goto badline; + } + + if (! device->parse_row(table_id, ! table_dirty, p)) { + goto badline; + } + table_dirty = 1; + } + } + fclose(conf); +} + +// +// Print full information about the device configuration. +// +void radio_print_config(FILE *out, int verbose) +{ + if (verbose) { + char buf [40]; + time_t t; + struct tm *tmp; + + t = time(NULL); + tmp = localtime(&t); + if (! tmp || ! strftime(buf, sizeof(buf), "%Y/%m/%d ", tmp)) + buf[0] = 0; + fprintf(out, "#\n"); + fprintf(out, "# This configuration was generated %sby dmrconfig,\n", buf); + fprintf(out, "# Version %s, %s\n", version, copyright); + fprintf(out, "#\n"); + } + device->print_config(out, verbose); +} diff --git a/radio.h b/radio.h new file mode 100644 index 0000000..c8ca0a4 --- /dev/null +++ b/radio.h @@ -0,0 +1,108 @@ +/* + * Configuration Utility for DMR radios. + * + * 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. + */ + +// +// Connect to the radio via the serial port. +// Identify the type of device. +// +void radio_connect(const char *port_name, const char *type); + +// +// Close the serial port. +// +void radio_disconnect(void); + +// +// Read firmware image from the device. +// +void radio_download(void); + +// +// Write firmware image to the device. +// +void radio_upload(int cont_flag); + +// +// Print a generic information about the device. +// +void radio_print_version(FILE *out); + +// +// Print full information about the device configuration. +// +void radio_print_config(FILE *out, int verbose); + +// +// Read firmware image from the binary file. +// +void radio_read_image(char *filename); + +// +// Save firmware image to the binary file. +// +void radio_save_image(char *filename); + +// +// Read the configuration from text file, and modify the firmware. +// +void radio_parse_config(char *filename); + +// +// Device-dependent interface to the radio. +// +typedef struct { + const char *name; + void (*download)(void); + void (*upload)(int cont_flag); + int (*is_compatible)(void); + void (*read_image)(FILE *img); + void (*save_image)(FILE *img); + void (*print_version)(FILE *out); + void (*print_config)(FILE *out, int verbose); + void (*parse_parameter)(char *param, char *value); + int (*parse_header)(char *line); + int (*parse_row)(int table_id, int first_row, char *line); +} radio_device_t; + +extern radio_device_t radio_md380; // TYT MD-380 +extern radio_device_t radio_uv380; // TYT MD-UV380 + +// +// Radio: memory contents. +// +extern unsigned char radio_mem[]; + +// +// File descriptor of serial port with programming cable attached. +// +int radio_port; + +// +// Read/write progress counter. +// +int radio_progress; diff --git a/util.c b/util.c new file mode 100644 index 0000000..f334e41 --- /dev/null +++ b/util.c @@ -0,0 +1,224 @@ +/* + * Auxiliary functions. + * + * 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 +#ifdef MINGW32 +# include +#else +# include +#endif +#include "util.h" + +// +// Check for a regular file. +// +int is_file(char *filename) +{ +#ifdef MINGW32 + // Treat COM* as a device. + return strncasecmp(filename, "com", 3) != 0; +#else + struct stat st; + + if (stat(filename, &st) < 0) { + // File not exist: treat it as a regular file. + return 1; + } + return (st.st_mode & S_IFMT) == S_IFREG; +#endif +} + +// +// Print data in hex format. +// +void print_hex(const unsigned char *data, int len) +{ + int i; + + printf("%02x", (unsigned char) data[0]); + for (i=1; i> 28) & 15) * 10000000 + + ((bcd >> 24) & 15) * 1000000 + + ((bcd >> 20) & 15) * 100000 + + ((bcd >> 16) & 15) * 10000 + + ((bcd >> 12) & 15) * 1000 + + ((bcd >> 8) & 15) * 100 + + ((bcd >> 4) & 15) * 10 + + (bcd & 15); +} + +// +// Convert 32-bit value from integer +// binary coded decimal format (8 digits). +// +int int_to_bcd(int val) +{ + return ((val / 10000000) % 10) << 28 | + ((val / 1000000) % 10) << 24 | + ((val / 100000) % 10) << 20 | + ((val / 10000) % 10) << 16 | + ((val / 1000) % 10) << 12 | + ((val / 100) % 10) << 8 | + ((val / 10) % 10) << 4 | + (val % 10); +} + +// +// Get a binary value of the parameter: On/Off, +// Ignore case. +// For invlid value, print a message and halt. +// +int on_off(char *param, char *value) +{ + if (strcasecmp("On", value) == 0) + return 1; + if (strcasecmp("Off", value) == 0) + return 0; + fprintf(stderr, "Bad value for %s: %s\n", param, value); + exit(-1); +} + +// +// Get integer value, or "Off" as 0, +// Ignore case. +// +int atoi_off(const char *value) +{ + if (strcasecmp("Off", value) == 0) + return 0; + return atoi(value); +} + +// +// Copy a text string to memory image. +// Clear unused part with spaces. +// +void copy_str(unsigned char *dest, const char *src, int nbytes) +{ + int i; + + for (i=0; i 0) + fprintf(out, ","); + fprintf(out, " %s", tab[i]); + } + fprintf(out, "\n"); +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..c8fd4c8 --- /dev/null +++ b/util.h @@ -0,0 +1,134 @@ +/* + * Auxiliary functions. + * + * 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. + */ + +// +// Localization. +// +#if 0 +#include +#define _(str) gettext(str) +#else +#define _(str) str +#define setlocale(x,y) /* empty */ +#define bindtextdomain(x,y) /* empty */ +#define textdomain(x) /* empty */ +#endif + +// +// Program version. +// +extern const char version[]; +extern const char *copyright; + +// +// Trace data i/o via the serial port. +// +int serial_verbose; + +// +// Print data in hex format. +// +void print_hex(const unsigned char *data, int len); + +// +// Open the serial port. +// +int serial_open(void); + +// +// Close the serial port. +// +void serial_close(int fd); + +// +// Read data from serial port. +// Return 0 when no data available. +// Use 200-msec timeout. +// +int serial_read(int fd, unsigned char *data, int len); + +// +// Write data to serial port. +// +void serial_write(int fd, const void *data, int len); + +// +// Delay in milliseconds. +// +void mdelay(unsigned msec); + +// +// Check for a regular file. +// +int is_file(char *filename); + +// +// Convert 32-bit value from binary coded decimal +// to integer format (8 digits). +// +int bcd_to_int(int bcd); + +// +// Convert 32-bit value from integer +// binary coded decimal format (8 digits). +// +int int_to_bcd(int val); + +// +// Get a binary value of the parameter: On/Off, +// Ignore case. +// +int on_off(char *param, char *value); + +// +// Get integer value, or "Off" as 0, +// Ignore case. +// +int atoi_off(const char *value); + +// +// Copy a text string to memory image. +// Clear unused space to zero. +// +void copy_str(unsigned char *dest, const char *src, int nbytes); + +// +// Find a string in a table of size nelem, ignoring case. +// Return -1 when not found. +// +int string_in_table(const char *value, const char *tab[], int nelem); + +// +// Print description of the parameter. +// +void print_options(FILE *out, const char **tab, int num, const char *info); + +// +// Print list of all squelch tones. +// +void print_squelch_tones(FILE *out, int normal_only); diff --git a/uv380.c b/uv380.c new file mode 100644 index 0000000..bc2b4c6 --- /dev/null +++ b/uv380.c @@ -0,0 +1,856 @@ +/* + * Interface to TYT MD-UV380. + * + * 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 "radio.h" +#include "util.h" + +#define NCHAN 1000 +#define NZONES 10 +#define NPMS 50 +#define MEMSZ 0x6fc8 + +#define OFFSET_VFO 0x0048 +#define OFFSET_HOME 0x01c8 +#define OFFSET_CHANNELS 0x0248 +#define OFFSET_PMS 0x40c8 +#define OFFSET_NAMES 0x4708 +#define OFFSET_ZONES 0x69c8 +#define OFFSET_SCAN 0x6ec8 + +static const char CHARSET[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ !`o$%&'()*+,-./|;/=>?@[~]^__"; +#define NCHARS 65 +#define SPACE 36 +#define OPENBOX 64 + +static const char *POWER_NAME[] = { "High", "Med", "Low", "??" }; + +static const char *SCAN_NAME[] = { "+", "-", "Only", "??" }; + +enum { + STEP_5 = 0, + STEP_10, + STEP_12_5, + STEP_15, + STEP_20, + STEP_25, + STEP_50, + STEP_100, +}; + +// +// Data structure for a memory channel. +// +typedef struct { + uint8_t duplex : 4, // Repeater mode +#define D_SIMPLEX 0 +#define D_NEG_OFFSET 2 +#define D_POS_OFFSET 3 +#define D_CROSS_BAND 4 + isam : 1, // Amplitude modulation + isnarrow : 1, // Narrow FM modulation + _u1 : 1, + used : 1; // Channel is used + uint8_t rxfreq [3]; // Receive frequency + uint8_t tmode : 3, // CTCSS/DCS mode +#define T_OFF 0 +#define T_TONE 1 +#define T_TSQL 2 +#define T_TSQL_REV 3 +#define T_DTCS 4 +#define T_D 5 +#define T_T_DCS 6 +#define T_D_TSQL 7 + step : 3, // Frequency step + _u2 : 2; + uint8_t txfreq [3]; // Transmit frequency when cross-band + uint8_t tone : 6, // CTCSS tone select +#define TONE_DEFAULT 12 + + power : 2; // Transmit power level + uint8_t dtcs : 7, // DCS code select + _u3 : 1; + uint8_t _u4 [2]; + uint8_t offset; // TX offset, in 50kHz steps + uint8_t _u5 [3]; +} memory_channel_t; + +// +// Data structure for a channel name. +// +typedef struct { + uint8_t name[6]; + uint8_t _u1 : 7, + used : 1; + uint8_t _u2 : 7, + valid : 1; +} memory_name_t; + +// +// Print a generic information about the device. +// +static void uv380_print_version(FILE *out) +{ + // Nothing to print. +} + +// +// Read block of data, up to 64 bytes. +// When start==0, return non-zero on success or 0 when empty. +// When start!=0, halt the program on any error. +// +static int read_block(int fd, int start, unsigned char *data, int nbytes) +{ + unsigned char reply; + int len; + + // Read data. + len = serial_read(fd, data, nbytes); + if (len != nbytes) { + if (start == 0) + return 0; + fprintf(stderr, "Reading block 0x%04x: got only %d bytes.\n", start, len); + exit(-1); + } + + // Get acknowledge. + serial_write(fd, "\x06", 1); + if (serial_read(fd, &reply, 1) != 1) { + fprintf(stderr, "No acknowledge after block 0x%04x.\n", start); + exit(-1); + } + if (reply != 0x06) { + fprintf(stderr, "Bad acknowledge after block 0x%04x: %02x\n", start, reply); + exit(-1); + } + if (serial_verbose) { + printf("# Read 0x%04x: ", start); + print_hex(data, nbytes); + printf("\n"); + } else { + ++radio_progress; + if (radio_progress % 16 == 0) { + fprintf(stderr, "#"); + fflush(stderr); + } + } + return 1; +} + +// +// Write block of data, up to 64 bytes. +// Halt the program on any error. +// Return 0 on error. +// +static int write_block(int fd, int start, const unsigned char *data, int nbytes) +{ + unsigned char reply[64]; + int len; + + serial_write(fd, data, nbytes); + + // Get echo. + len = serial_read(fd, reply, nbytes); + if (len != nbytes) { + fprintf(stderr, "! Echo for block 0x%04x: got only %d bytes.\n", start, len); + return 0; + } + + // Get acknowledge. + if (serial_read(fd, reply, 1) != 1) { + fprintf(stderr, "! No acknowledge after block 0x%04x.\n", start); + return 0; + } + if (reply[0] != 0x06) { + fprintf(stderr, "! Bad acknowledge after block 0x%04x: %02x\n", start, reply[0]); + return 0; + } + if (serial_verbose) { + printf("# Write 0x%04x: ", start); + print_hex(data, nbytes); + printf("\n"); + } else { + ++radio_progress; + if (radio_progress % 16 == 0) { + fprintf(stderr, "#"); + fflush(stderr); + } + } + return 1; +} + +// +// Read memory image from the device. +// +static void uv380_download() +{ + int addr; + + // Wait for the first 8 bytes. + while (read_block(radio_port, 0, &radio_mem[0], 8) == 0) + continue; + + // Get the rest of data. + for (addr=8; addr> 4) & 15) * 10000000 + + (bcd[1] & 15) * 1000000 + + ((bcd[2] >> 4) & 15) * 100000 + + (bcd[2] & 15) * 10000; + hz += (bcd[0] >> 6) * 2500; + return hz; +} + +// +// Convert an integet frequency value (in Hertz) +// to a 3-byte binary coded decimal format. +// +static void hz_to_freq(int hz, uint8_t *bcd) +{ + bcd[0] = (hz / 2500 % 4) << 6 | + (hz / 100000000 % 10); + bcd[1] = (hz / 10000000 % 10) << 4 | + (hz / 1000000 % 10); + bcd[2] = (hz / 100000 % 10) << 4 | + (hz / 10000 % 10); +} + +// +// Is this zone non-empty? +// +static int have_zone(int b) +{ + unsigned char *data = &radio_mem[OFFSET_ZONES + b * 0x80]; + int c; + + for (c=0; c= 0) + fprintf(out, ","); + fprintf(out, "%d", cnum); + } + last = cnum; + } + if (range) + fprintf(out, "-%d", last); + fprintf(out, "\n"); +} + +// +// Set the bitmask of zones for a given channel. +// Return 0 on failure. +// +static void setup_zone(int zone_index, int chan_index) +{ + uint8_t *data = &radio_mem[OFFSET_ZONES + zone_index*0x80 + chan_index/8]; + + *data |= 1 << (chan_index & 7); +} + +// +// Extract channel name. +// +static void decode_name(int i, char *name) +{ + memory_name_t *nm = i + (memory_name_t*) &radio_mem[OFFSET_NAMES]; + + if (nm->valid && nm->used) { + int n, c; + for (n=0; n<6; n++) { + c = nm->name[n]; + name[n] = (c < NCHARS) ? CHARSET[c] : ' '; + + // Replace spaces by underscore. + if (name[n] == ' ') + name[n] = '_'; + } + // Strip trailing spaces. + for (n=5; n>=0 && name[n]=='_'; n--) + name[n] = 0; + name[6] = 0; + } +} + +// +// Encode a character from ASCII to internal index. +// Replace underscores by spaces. +// Make all letters uppercase. +// +static int encode_char(int c) +{ + int i; + + // Replace underscore by space. + if (c == '_') + c = ' '; + if (c >= 'a' && c <= 'z') + c += 'A' - 'a'; + for (i=0; ivalid = 1; + nm->used = 1; + for (n=0; n<6 && name[n]; n++) { + nm->name[n] = encode_char(name[n]); + } + for (; n<6; n++) + nm->name[n] = SPACE; + } else { + // Clear name. + nm->valid = 0; + nm->used = 0; + for (n=0; n<6; n++) + nm->name[n] = 0xff; + } +} + +// +// Get all parameters for a given channel. +// Seek selects the type of channel: +// OFFSET_VFO - VFO channel, 0..4 +// OFFSET_HOME - home channel, 0..4 +// OFFSET_CHANNELS - memory channel, 0..999 +// OFFSET_PMS - programmable memory scan, i=0..99 +// +static void decode_channel(int i, int seek, char *name, + int *rx_hz, int *tx_hz, int *power, int *wide, + int *scan, int *isam, int *step) +{ + memory_channel_t *ch = i + (memory_channel_t*) &radio_mem[seek]; + int scan_data = radio_mem[OFFSET_SCAN + i/4]; + + *rx_hz = *tx_hz = 0; + *power = *wide = *scan = *isam = *step = 0; + if (name) + *name = 0; + if (! ch->used && (seek == OFFSET_CHANNELS || seek == OFFSET_PMS)) + return; + + // Extract channel name. + if (name && seek == OFFSET_CHANNELS) + decode_name(i, name); + + // Decode channel frequencies. + *rx_hz = freq_to_hz(ch->rxfreq); + + *tx_hz = *rx_hz; + switch (ch->duplex) { + case D_NEG_OFFSET: + *tx_hz -= ch->offset * 50000; + break; + case D_POS_OFFSET: + *tx_hz += ch->offset * 50000; + break; + case D_CROSS_BAND: + *tx_hz = freq_to_hz(ch->txfreq); + break; + } + + // Other parameters. + *power = ch->power; + *wide = ! ch->isnarrow; + *scan = (scan_data << ((i & 3) * 2) >> 6) & 3; + *isam = ch->isam; + *step = ch->step; +} + +// +// Set the parameters for a given memory channel. +// +static void setup_channel(int i, char *name, double rx_mhz, double tx_mhz, + int tmode, int power, int wide, int scan, int isam) +{ + memory_channel_t *ch = i + (memory_channel_t*) &radio_mem[OFFSET_CHANNELS]; + + hz_to_freq((int) (rx_mhz * 1000000.0), ch->rxfreq); + + double offset_mhz = tx_mhz - rx_mhz; + ch->offset = 0; + ch->txfreq[0] = ch->txfreq[1] = ch->txfreq[2] = 0; + if (offset_mhz == 0) { + ch->duplex = D_SIMPLEX; + } else if (offset_mhz > 0 && offset_mhz < 256 * 0.05) { + ch->duplex = D_POS_OFFSET; + ch->offset = (int) (offset_mhz / 0.05 + 0.5); + } else if (offset_mhz < 0 && offset_mhz > -256 * 0.05) { + ch->duplex = D_NEG_OFFSET; + ch->offset = (int) (-offset_mhz / 0.05 + 0.5); + } else { + ch->duplex = D_CROSS_BAND; + hz_to_freq((int) (tx_mhz * 1000000.0), ch->txfreq); + } + ch->used = (rx_mhz > 0); + ch->tmode = tmode; + ch->power = power; + ch->isnarrow = ! wide; + ch->isam = isam; + ch->step = (rx_mhz >= 400) ? STEP_12_5 : STEP_5; + ch->_u1 = 0; + ch->_u2 = (rx_mhz >= 400); + ch->_u3 = 0; + ch->_u4[0] = 15; + ch->_u4[1] = 0; + ch->_u5[0] = ch->_u5[1] = ch->_u5[2] = 0; + + // Scan mode. + unsigned char *scan_data = &radio_mem[OFFSET_SCAN + i/4]; + int scan_shift = (i & 3) * 2; + *scan_data &= ~(3 << scan_shift); + *scan_data |= scan << scan_shift; + + encode_name(i, name); +} + +// +// Print the transmit offset or frequency. +// +static void print_offset(FILE *out, int rx_hz, int tx_hz) +{ + int delta = tx_hz - rx_hz; + + if (delta == 0) { + fprintf(out, "+0 "); + } else if (delta > 0 && delta/50000 <= 255) { + if (delta % 1000000 == 0) + fprintf(out, "+%-7u", delta / 1000000); + else + fprintf(out, "+%-7.3f", delta / 1000000.0); + } else if (delta < 0 && -delta/50000 <= 255) { + delta = - delta; + if (delta % 1000000 == 0) + fprintf(out, "-%-7u", delta / 1000000); + else + fprintf(out, "-%-7.3f", delta / 1000000.0); + } else { + // Cross band mode. + fprintf(out, " %-7.4f", tx_hz / 1000000.0); + } +} + +// +// Print full information about the device configuration. +// +static void uv380_print_config(FILE *out, int verbose) +{ + int i; + + fprintf(out, "Radio: TYT MD-UV380\n"); + + // + // Memory channels. + // + fprintf(out, "\n"); + if (verbose) { + fprintf(out, "# Table of preprogrammed channels.\n"); + fprintf(out, "# 1) Channel number: 1-%d\n", NCHAN); + fprintf(out, "# 2) Name: up to 6 characters, no spaces\n"); + fprintf(out, "# 3) Receive frequency in MHz\n"); + fprintf(out, "# 4) Transmit frequency or +/- offset in MHz\n"); + fprintf(out, "# 5) Squelch tone for receive, or '-' to disable\n"); + fprintf(out, "# 6) Squelch tone for transmit, or '-' to disable\n"); + fprintf(out, "# 7) Transmit power: High, Mid, Low\n"); + fprintf(out, "# 8) Modulation: Wide, Narrow, AM\n"); + fprintf(out, "# 9) Scan mode: +, -, Only\n"); + fprintf(out, "#\n"); + } + fprintf(out, "Channel Name Receive Transmit Power Modulation Scan\n"); + for (i=0; i= 108 && mhz <= 520) + return 1; + if (mhz >= 700 && mhz <= 999) + return 1; + return 0; +} + +// +// Parse one line of memory channel table. +// Start_flag is 1 for the first table row. +// Return 0 on failure. +// +static int parse_channel(int first_row, char *line) +{ + char num_str[256], name_str[256], rxfreq_str[256], offset_str[256]; + char power_str[256], wide_str[256], scan_str[256]; + int num, tmode, power, wide, scan, isam; + double rx_mhz, tx_mhz; + + if (sscanf(line, "%s %s %s %s %s %s %s", + num_str, name_str, rxfreq_str, offset_str, power_str, + wide_str, scan_str) != 9) + 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; + + //TODO + tmode = 0; + + if (strcasecmp("High", power_str) == 0) { + power = 0; + } else if (strcasecmp("Mid", power_str) == 0) { + power = 1; + } else if (strcasecmp("Low", power_str) == 0) { + power = 2; + } else { + fprintf(stderr, "Bad power level.\n"); + return 0; + } + + if (strcasecmp("Wide", wide_str) == 0) { + wide = 1; + isam = 0; + } else if(strcasecmp("Narrow", wide_str) == 0) { + wide = 0; + isam = 0; + } else if(strcasecmp("AM", wide_str) == 0) { + wide = 1; + isam = 1; + } else { + fprintf(stderr, "Bad modulation width.\n"); + return 0; + } + + if (*scan_str == '+') { + scan = 0; + } else if (*scan_str == '-') { + scan = 1; + } else if (strcasecmp("Only", scan_str) == 0) { + scan = 2; + } else { + fprintf(stderr, "Bad scan flag.\n"); + return 0; + } + + if (first_row) { + // On first entry, erase the channel table. + int i; + for (i=0; i NZONES) { + fprintf(stderr, "Bad zone number.\n"); + return 0; + } + + if (first_row) { + // On first entry, erase the Zones table. + memset(&radio_mem[OFFSET_ZONES], 0, NZONES * 0x80); + } + + if (*chan_str == '-') + return 1; + + 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", bnum, str); + return 0; + } + if (cnum < 1 || cnum > NCHAN) { + fprintf(stderr, "Zone %d: wrong channel number %d.\n", bnum, cnum); + return 0; + } + + if (range) { + // Add range. + int c; + for (c=last; c