├── .gitignore ├── Change FREQ.jpg ├── LICENSE ├── README.md ├── SA818_SR110_Tools.zip ├── SR_FRS_1WU (UHF) walkie talkie transceiver & data transfer module spec. V106.pdf └── srfrs.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *~ 3 | *.pyc 4 | /*.egg-info/ 5 | /build/ 6 | /dist/ 7 | upload.sh -------------------------------------------------------------------------------- /Change FREQ.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jumbo5566/SRFRS/c5cd421f54afc40f4277b2c3aff0bba796c2154d/Change FREQ.jpg -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020, Fred Cirera 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WINDOWS SA-818 SA818 SR110 SR-FRS Radio Module Programming Tools for win7 win10 x64 2 | 3 | 4 | 2023/02 add new Windows x64 tools for SA818 SA-818 SRFRS SR110 both Module Support now! 5 | 6 | 7 | ## pls download SA818_SR110_Tools.zip 8 | 9 | ## use usb cable connect COM port install CH340 USB Driver to SET Freq 10 | -------------------------------------------------------------------------- 11 | My friend (W6IPA) and I developed a versatile Raspberry-Pi hat that 12 | can be used for Allstar, Echolink, APRS, or any digital modes. 13 | 14 | We use the program in this GitHub repository to program the radio 15 | module SR-FRS used for the Pi-Hat. 16 | 17 | **Before programming the SR-FRS module, make sure you consult the band 18 | plan for your country and transmit on a frequency you are allowed to 19 | use.** 20 | 21 | ## Intallation 22 | 23 | ``` 24 | $ git clone https://github.com/jumbo5566/SRFRS 25 | 26 | ``` 27 | 28 | ## Example 29 | 30 | ``` 31 | [root@allstar ~]# python srfrs.py version 32 | SRFRS: INFO: Firmware version: 110U-V223 33 | 34 | [root@alarmpi SRFRS]# python srfrs.py radio --frequency 438.500 35 | SRFRS: INFO: +DMOSETGROUP:0, RX frequency: 438.5000, TX frequency: 438.5000, squelch: 4, OK 36 | 37 | [root@alarmpi SRFRS]# python srfrs.py radio --frequency 438.500 --offset -5 38 | SRFRS: INFO: +DMOSETGROUP:0, RX frequency: 438.5000, TX frequency: 433.5000, squelch: 4, OK 39 | 40 | [root@allstar ~]# python srfrs.py radio --frequency 145.230 --offset -.6 --ctcss 100 41 | SRFRS: INFO: +DMOSETGROUP:0, RX frequency: 145.2300, TX frequency: 144.6300, ctcss: 100.0, squelch: 4, OK 42 | 43 | [root@allstar ~]# python srfrs.py volume --level 5 44 | SRFRS: INFO: +DMOSETVOLUME:0 Volume level: 5 45 | ``` 46 | 47 | If you use an FTDI dongle to program the SRFRS module the USB port can 48 | be specified with the `--port` argument 49 | 50 | ``` 51 | [root@allstar ~]# python srfrs.py --port /dev/ttyAMA0 volume --level 5 52 | SRFRS: INFO: +DMOSETVOLUME:0 Volume level: 5 53 | ``` 54 | 55 | ## Usage 56 | 57 | This program has for sections: 58 | 59 | - radio: Program the radio's frequency, tone and squelch level 60 | - volume: Set the volume level 61 | - version: display the firmware version of the SR-FRS module 62 | 63 | ``` 64 | usage: srfrs [-h] [--port PORT] [--debug] 65 | {radio,volume,filters,version} ... 66 | 67 | generate configuration for switch port 68 | 69 | positional arguments: 70 | {radio,volume,filters,version} 71 | radio Program the radio (frequency/tome/squelch) 72 | volume Set the volume level 73 | filters Set filters 74 | version Show the firmware version of the SA818 75 | 76 | optional arguments: 77 | -h, --help show this help message and exit 78 | --port PORT Serial port [default: linux console port] 79 | --debug 80 | ``` 81 | 82 | ### Radio 83 | 84 | ``` 85 | usage: srfrs radio [-h] --frequency FREQUENCY [--offset OFFSET] 86 | [--squelch SQUELCH] [--ctcss CTCSS | --dcs DCS] 87 | 88 | optional arguments: 89 | -h, --help show this help message and exit 90 | --frequency FREQUENCY 91 | Transmit frequency 92 | --offset OFFSET 0.0 for no offset [default: 0.0] 93 | --squelch SQUELCH Squelch value (1 to 9) [default: 4] 94 | --ctcss CTCSS CTCSS (PL Tone) 0 for no CTCSS [default: None] 95 | --dcs DCS DCS code must me the number followed by [N normal] or 96 | [I inverse] [default: None] 97 | ``` 98 | 99 | ### Volume 100 | 101 | ``` 102 | usage: srfrs volume [-h] [--level LEVEL] 103 | 104 | optional arguments: 105 | -h, --help show this help message and exit 106 | --level LEVEL Volume value (1 to 8) [default: 4] 107 | ``` 108 | 109 | 110 | ## CTCSS codes (PL Tones 38 Sets) 111 | 112 | ``` 113 | 67.0, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8, 97.4, 114 | 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 115 | 141.3, 146.2, 151.4, 156.7, 162.2, 167.9, 173.8, 179.9, 186.2, 192.8, 116 | 203.5, 210.7, 218.1, 225.7, 233.6, 241.8, 250.3 117 | ``` 118 | 119 | ## DCS Codes not support YET 120 | 121 | 122 | 123 | 124 | [1]: http://www.sunrisedigit.com/upload/file/1558943384.rar 125 | -------------------------------------------------------------------------------- /SA818_SR110_Tools.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jumbo5566/SRFRS/c5cd421f54afc40f4277b2c3aff0bba796c2154d/SA818_SR110_Tools.zip -------------------------------------------------------------------------------- /SR_FRS_1WU (UHF) walkie talkie transceiver & data transfer module spec. V106.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jumbo5566/SRFRS/c5cd421f54afc40f4277b2c3aff0bba796c2154d/SR_FRS_1WU (UHF) walkie talkie transceiver & data transfer module spec. V106.pdf -------------------------------------------------------------------------------- /srfrs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Fred (W6BSD) 3 | # SR-FRS by BG7IYN 4 | 5 | __doc__ = """srfrs""" 6 | 7 | import argparse 8 | import logging 9 | import os 10 | import sys 11 | import textwrap 12 | import time 13 | 14 | import serial 15 | 16 | logging.basicConfig(format='%(name)s: %(levelname)s: %(message)s', 17 | level=logging.INFO) 18 | logger = logging.getLogger('SRFRS') 19 | 20 | 21 | CTCSS = ( 22 | "None", "67.0", "71.9", "74.4", "77.0", "79.7", "82.5", "85.4", "88.5", "91.5", 23 | "94.8", "97.4", "100.0", "103.5", "107.2", "110.9", "114.8", "118.8", "123.0", 24 | "127.3", "131.8", "136.5", "141.3", "146.2", "151.4", "156.7", "162.2", 25 | "167.9", "173.8", "179.9", "186.2", "192.8", "203.5", "210.7", "218.1", 26 | "225.7", "233.6", "241.8", "250.3" 27 | ) 28 | 29 | DCS_CODES = [ 30 | "023", "025", "026", "031", "032", "036", "043", "047", "051", "053", "054", "065", "071", 31 | "072", "073", "074", "114", "115", "116", "122", "125", "131", "132", "134", "143", "145", 32 | "152", "155", "156", "162", "165", "172", "174", "205", "212", "223", "225", "226", "243", 33 | "244", "245", "246", "251", "252", "255", "261", "263", "265", "266", "271", "274", "306", 34 | "311", "315", "325", "331", "332", "343", "346", "351", "356", "364", "365", "371", "411", 35 | "412", "413", "423", "431", "432", "445", "446", "452", "454", "455", "462", "464", "465", 36 | "466", "503", "506", "516", "523", "526", "532", "546", "565", "606", "612", "624", "627", 37 | "631", "632", "654", "662", "664", "703", "712", "723", "731", "732", "734", "743", "754" 38 | ] 39 | 40 | DEFAULT_BAUDRATE = 9600 41 | 42 | class SRFRS: 43 | EOL = "\r\n" 44 | INIT = "AT+DMOCONNECT" 45 | SETGRP = "AT+DMOSETGROUP" 46 | FILTER = "AT+SETFILTER" 47 | VOLUME = "AT+DMOSETVOLUME" 48 | TAIL = "AT+SETTAIL" 49 | NARROW = 0 50 | WIDE = 1 51 | PORTS = ('/dev/serial0', '/dev/ttyUSB0') 52 | READ_TIMEOUT = 1.0 53 | 54 | def __init__(self, port=None): 55 | self.serial = None 56 | if port: 57 | ports = [port] 58 | else: 59 | ports = self.PORTS 60 | 61 | for _port in ports: 62 | try: 63 | self.serial = serial.Serial(port=_port, baudrate=DEFAULT_BAUDRATE, 64 | parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, 65 | bytesize=serial.EIGHTBITS, 66 | timeout=self.READ_TIMEOUT) 67 | break 68 | except serial.SerialException as err: 69 | logger.debug(err) 70 | 71 | if not isinstance(self.serial, serial.Serial): 72 | raise IOError('Error openning the serial port') 73 | 74 | self.send(self.INIT) 75 | reply = self.readline() 76 | if reply != "+DMOCONNECT:0": 77 | raise SystemError('Connection error') 78 | 79 | def close(self): 80 | self.serial.close() 81 | 82 | def send(self, *args): 83 | data = ','.join(args) 84 | logger.debug('Sending: %s', data) 85 | data = bytes(data + self.EOL, 'ascii') 86 | try: 87 | self.serial.write(data) 88 | except serial.SerialException as err: 89 | logger.error(err) 90 | 91 | def readline(self): 92 | try: 93 | line = self.serial.readline() 94 | except serial.SerialException as err: 95 | logger.warning(err) 96 | return None 97 | line = line.decode('ascii') 98 | logger.debug(line) 99 | return line.rstrip() 100 | 101 | def version(self): 102 | self.send("AT+DMOVERQ") 103 | time.sleep(0.5) 104 | reply = self.readline() 105 | try: 106 | _, version = reply.split(':') 107 | except ValueError: 108 | logger.error('Unable to decode the firmeare version') 109 | else: 110 | logger.info('Firmware version: %s', version) 111 | return version 112 | 113 | def set_radio(self, opts): 114 | tone = opts.ctcss if opts.ctcss else opts.dcs 115 | if not tone: # 00 = No ctcss or dcs tone 116 | tone = '00' 117 | 118 | if opts.offset == 0.0: 119 | tx_freq = rx_freq = "{:.4f}".format(opts.frequency) 120 | else: 121 | rx_freq = "{:.4f}".format(opts.frequency) 122 | tx_freq = "{:.4f}".format(opts.frequency + opts.offset) 123 | 124 | cmd = "{}={},{},{},{},{},{},{}".format(self.SETGRP, self.WIDE, tx_freq, rx_freq, 125 | tone, opts.squelch, tone,1) 126 | self.send(cmd) 127 | time.sleep(1) 128 | response = self.readline() 129 | if response != '+DMOSETGROUP:0': 130 | logger.error('SR-FRS programming error') 131 | else: 132 | if opts.ctcss: 133 | msg = "%s, RX frequency: %s, TX frequency: %s, ctcss: %s, squelch: %s, OK" 134 | tone = CTCSS[int(tone)] 135 | logger.info(msg, response, rx_freq, tx_freq, tone, opts.squelch) 136 | elif opts.dcs: 137 | msg = "%s, RX frequency: %s, TX frequency: %s, dcs: %s, squelch: %s, OK" 138 | logger.info(msg, response, rx_freq, tx_freq, tone, opts.squelch) 139 | else: 140 | msg = "%s, RX frequency: %s, TX frequency: %s, squelch: %s, OK" 141 | logger.info(msg, response, rx_freq, tx_freq, opts.squelch) 142 | 143 | if opts.close_tail is not None and opts.ctcss is not None: 144 | self.close_tail(opts) 145 | elif opts.close_tail is not None: 146 | logger.warning('Ignoring "--close-tail" specified without ctcss') 147 | 148 | def set_filter(self, opts): 149 | _yn = {True: "Yes", False: "No"} 150 | # filters are pre-emphasis, high-pass, low-pass 151 | cmd = "{}={},{},{}".format(self.FILTER, int(not opts.emphasis), 152 | int(not opts.highpass), int(not opts.lowpass)) 153 | self.send(cmd) 154 | time.sleep(1) 155 | response = self.readline() 156 | if response != "+DMOSETFILTER:0": 157 | logger.error('SRFRS set filter error') 158 | else: 159 | logger.info("%s filters [Pre/De]emphasis: %s, high-pass: %s, low-pass: %s", 160 | response, _yn[opts.emphasis], _yn[opts.highpass], _yn[opts.lowpass]) 161 | 162 | def set_volume(self, opts): 163 | cmd = "{}={:d}".format(self.VOLUME, opts.level) 164 | self.send(cmd) 165 | time.sleep(1) 166 | response = self.readline() 167 | if response != "+DMOSETVOLUME:0": 168 | logger.error('SR-FRS set volume error') 169 | else: 170 | logger.info("%s Volume level: %d, OK", response, opts.level) 171 | 172 | def close_tail(self, opts): 173 | _yn = {True: "Yes", False: "No"} 174 | cmd = "{}={}".format(self.TAIL, int(opts.close_tail)) 175 | self.send(cmd) 176 | time.sleep(1) 177 | response = self.readline() 178 | if response != "+DMOSETTAIL:0": 179 | logger.error('SRFRS set filter error') 180 | else: 181 | logger.info("%s close tail: %s", response, _yn[opts.close_tail]) 182 | 183 | 184 | def type_frequency(parg): 185 | try: 186 | frequency = float(parg) 187 | except ValueError: 188 | raise argparse.ArgumentTypeError from None 189 | 190 | if not 136 < frequency < 174 and not 400 < frequency < 470: 191 | logger.error('Frequency outside the amateur bands') 192 | raise argparse.ArgumentError 193 | return frequency 194 | 195 | def type_ctcss(parg): 196 | err_msg = 'Invalid CTCSS use the --help argument for the list of CTCSS' 197 | try: 198 | ctcss = str(float(parg)) 199 | except ValueError: 200 | raise argparse.ArgumentTypeError from None 201 | 202 | if ctcss not in CTCSS: 203 | logger.error(err_msg) 204 | raise argparse.ArgumentError 205 | 206 | tone_code = CTCSS.index(ctcss) 207 | cc="{:02d}".format(int(tone_code)) 208 | print (cc) 209 | return "{:02d}".format(int(tone_code)) 210 | 211 | def type_dcs(parg): 212 | err_msg = 'Invalid DCS use the --help argument for the list of DCS' 213 | if parg[-1] not in ('N', 'I'): 214 | raise argparse.ArgumentError 215 | 216 | code, direction = parg[:-1], parg[-1] 217 | try: 218 | dcs = "{:03d}".format(int(code)) 219 | except ValueError: 220 | raise argparse.ArgumentTypeError from None 221 | 222 | if dcs not in DCS_CODES: 223 | logger.error(err_msg) 224 | raise argparse.ArgumentError 225 | 226 | dcs_code = dcs + direction 227 | return "{:s}".format(dcs_code) 228 | 229 | def type_squelch(parg): 230 | try: 231 | value = int(parg) 232 | except ValueError: 233 | raise argparse.ArgumentTypeError from None 234 | 235 | if value not in range(0, 9): 236 | logger.error('The value must must be between 0 and 8 (inclusive)') 237 | raise argparse.ArgumentError 238 | return value 239 | 240 | def type_level(parg): 241 | try: 242 | value = int(parg) 243 | except ValueError: 244 | raise argparse.ArgumentTypeError from None 245 | 246 | if value not in range(1, 9): 247 | logger.error('The value must must be between 1 and 8 (inclusive)') 248 | raise argparse.ArgumentError 249 | return value 250 | 251 | def yesno(parg): 252 | yes_strings = ["y", "yes", "true", "1", "on"] 253 | no_strings = ["n", "no", "false", "0", "off"] 254 | if parg.lower() in yes_strings: 255 | return True 256 | if parg.lower() in no_strings: 257 | return False 258 | raise argparse.ArgumentError 259 | 260 | def noneyesno(parg): 261 | if parg is not None: 262 | return yesno(parg) 263 | 264 | def set_loglevel(): 265 | loglevel = os.getenv('LOGLEVEL', 'INFO') 266 | loglevel = loglevel.upper() 267 | try: 268 | logger.root.setLevel(loglevel) 269 | except ValueError: 270 | logger.warning('Loglevel error: %s', loglevel) 271 | 272 | def format_codes(): 273 | ctcss = textwrap.wrap(', '.join(CTCSS[1:])) 274 | dcs = textwrap.wrap(', '.join(DCS_CODES)) 275 | 276 | codes = ( 277 | "CTCSS codes (PL Tones):\n{}".format('\n'.join(ctcss)), 278 | "\n\n", 279 | "DCS Codes:\n" 280 | "DCS codes must be followed by N or I for Normal or Inverse:\n", 281 | "> Example: 047I\n" 282 | "{}".format('\n'.join(dcs)) 283 | ) 284 | return ''.join(codes) 285 | 286 | def main(): 287 | set_loglevel() 288 | parser = argparse.ArgumentParser( 289 | description="generate configuration for switch port", 290 | epilog=format_codes(), 291 | formatter_class=argparse.RawDescriptionHelpFormatter, 292 | ) 293 | parser.add_argument("--port", type=str, 294 | help="Serial port [default: linux console port]") 295 | parser.add_argument("--debug", action="store_true", default=False) 296 | subparsers = parser.add_subparsers() 297 | 298 | p_radio = subparsers.add_parser("radio", help='Program the radio (frequency/tome/squelch)') 299 | p_radio.set_defaults(func="radio") 300 | p_radio.add_argument("--frequency", required=True, type=type_frequency, 301 | help="Receive frequency") 302 | p_radio.add_argument("--offset", default=0.0, type=float, 303 | help="Offset in MHz, 0 for no offset [default: %(default)s]") 304 | p_radio.add_argument("--squelch", type=type_squelch, default=4, 305 | help="Squelch value (0 to 8) [default: %(default)s]") 306 | code_group = p_radio.add_mutually_exclusive_group() 307 | code_group.add_argument("--ctcss", default=None, type=type_ctcss, 308 | help="CTCSS (PL Tone) 0 for no CTCSS [default: %(default)s]") 309 | code_group.add_argument("--dcs", default=None, type=type_dcs, 310 | help=("DCS code must me the number followed by [N normal] or " 311 | "[I inverse] [default: %(default)s]")) 312 | p_radio.add_argument("--close-tail", default=None, type=noneyesno, 313 | help="Close CTCSS Tail Tone (yes/no)") 314 | 315 | p_volume = subparsers.add_parser("volume", help="Set the volume level") 316 | p_volume.set_defaults(func="volume") 317 | p_volume.add_argument("--level", type=type_level, default=4, 318 | help="Volume value (1 to 8) [default: %(default)s]") 319 | 320 | p_filter = subparsers.add_parser("filters", help="Set/Unset filters") 321 | p_filter.set_defaults(func="filters") 322 | p_filter.add_argument("--emphasis", type=yesno, required=True, 323 | help="Disable [Pr/De]-emphasis (yes/no)") 324 | p_filter.add_argument("--highpass", type=yesno, required=True, 325 | help="Disable high pass filter (yes/no)") 326 | p_filter.add_argument("--lowpass", type=yesno, required=True, 327 | help="Disable low pass filters (yes/no)") 328 | 329 | p_version = subparsers.add_parser("version", help="Show the firmware version of the SRFRS") 330 | p_version.set_defaults(func="version") 331 | 332 | opts = parser.parse_args() 333 | if not hasattr(opts, 'func'): 334 | print('srfrs: error: the following arguments are required: {radio,volume,filters,version}\n' 335 | 'use --help for more informatiion', 336 | file=sys.stderr) 337 | exit(os.EX_USAGE) 338 | 339 | if opts.debug: 340 | logger.setLevel(logging.DEBUG) 341 | 342 | logger.debug(opts) 343 | 344 | try: 345 | radio = SRFRS(opts.port) 346 | except (IOError, SystemError) as err: 347 | logger.error(err) 348 | sys.exit(os.EX_IOERR) 349 | 350 | if opts.func == 'version': 351 | radio.version() 352 | elif opts.func == 'radio': 353 | radio.set_radio(opts) 354 | elif opts.func == 'filters': 355 | radio.set_filter(opts) 356 | elif opts.func == 'volume': 357 | radio.set_volume(opts) 358 | 359 | if __name__ == "__main__": 360 | main() 361 | --------------------------------------------------------------------------------