From abebad7dbd0ab8338a65bb62ae45c31cb3fe5c99 Mon Sep 17 00:00:00 2001 From: Serge Vakulenko Date: Mon, 20 Aug 2018 22:47:13 -0700 Subject: [PATCH] Remove unused code. --- .gitignore | 2 + Makefile | 34 +++ README.md | 48 +++ main.c | 174 +++++++++++ radio.c | 263 ++++++++++++++++ radio.h | 108 +++++++ util.c | 224 ++++++++++++++ util.h | 134 +++++++++ uv380.c | 856 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1843 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 main.c create mode 100644 radio.c create mode 100644 radio.h create mode 100644 util.c create mode 100644 util.h create mode 100644 uv380.c 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