mirror of
https://github.com/kk7ds/chirp
synced 2026-08-13 17:51:36 -04:00
parent
d89bb3cb38
commit
c523fff7b5
3 changed files with 480 additions and 3 deletions
476
chirp/drivers/ksun_m6.py
Normal file
476
chirp/drivers/ksun_m6.py
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
# Copyright 2012 Dan Smith <dsmith@danplanet.com>
|
||||
# Copyright 2024 Yuri D'Elia <wavexx@thregr.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from chirp import chirp_common, directory, memmap, bitwise, errors
|
||||
from chirp.settings import (
|
||||
RadioSetting, RadioSettings, RadioSettingGroup,
|
||||
RadioSettingValueBoolean, RadioSettingValueInteger, RadioSettingValueList
|
||||
)
|
||||
import struct
|
||||
|
||||
MEM_FORMAT = """
|
||||
struct {
|
||||
u8 _unk1;
|
||||
u8 voice: 2,
|
||||
beep: 1,
|
||||
_unk2: 1,
|
||||
vox: 4;
|
||||
u8 led_timeout: 4,
|
||||
led: 2,
|
||||
_unk3: 2;
|
||||
u8 _unk4: 4,
|
||||
sq: 4;
|
||||
u8 _unk5: 2,
|
||||
tot: 6;
|
||||
u8 _unk6: 4,
|
||||
lock_timeout: 4;
|
||||
u8 _unk7: 1,
|
||||
channel: 7;
|
||||
u8 _unk8: 5,
|
||||
bat_save: 3;
|
||||
u8 _unk9[2];
|
||||
u8 pass[6];
|
||||
} settings;
|
||||
|
||||
struct {
|
||||
u8 freq[5]; // 20 bit rx + 20 bit tx
|
||||
u16 low_pwr: 1,
|
||||
no_tx: 1,
|
||||
rx_tone: 2,
|
||||
rx_code: 12;
|
||||
u16 nfm: 1,
|
||||
skip: 1,
|
||||
tx_tone: 2,
|
||||
tx_code: 12;
|
||||
u8 _unk3: 1,
|
||||
compander: 1,
|
||||
_unk4: 1,
|
||||
hopping: 1,
|
||||
scrambler: 4;
|
||||
} memory[80];
|
||||
"""
|
||||
|
||||
VOICE_LIST = ["off", "Chinese", "English"]
|
||||
SCRAMBLER_LIST = ["off", "1", "2", "3", "4", "5", "6", "7", "8"]
|
||||
LED_LIST = ["Low", "Medium", "High"]
|
||||
BAT_SAVE_LIST = ["off", "1:1", "1:2", "1:3", "1:4"]
|
||||
TONE_LIST = ["Tone", "DTCS_N", "DTCS_I", ""]
|
||||
LED_TIMEOUT_LIST = ["Continuous", "5", "10", "15", "20", "25", "30",
|
||||
"35", "40", "45", "50", "55", "60"]
|
||||
LOCK_TIMEOUT_LIST = ["off", "5", "10", "15", "20", "25", "30",
|
||||
"35", "40", "45", "50", "55", "60"]
|
||||
TOT_LIST = ["off", "15", "30", "45", "60", "75", "90",
|
||||
"105", "120", "135", "150", "165", "180", "195",
|
||||
"210", "225", "240", "255", "270", "285",
|
||||
"300", "315", "330", "345", "360", "375", "390",
|
||||
"405", "420", "435", "450", "465", "480", "495",
|
||||
"510", "525", "540", "555", "570", "585", "600"]
|
||||
POWER_LIST = [chirp_common.PowerLevel("High", watts=2.00),
|
||||
chirp_common.PowerLevel("Low", watts=0.50)]
|
||||
|
||||
|
||||
def _checksum(data):
|
||||
cs = 2
|
||||
for byte in data:
|
||||
cs += byte
|
||||
return cs % 256
|
||||
|
||||
|
||||
def enter_programming_mode(radio):
|
||||
serial = radio.pipe
|
||||
|
||||
cmd = b"\x32\x31\x05\x10"
|
||||
req = cmd + bytes([_checksum(cmd)])
|
||||
|
||||
try:
|
||||
serial.write(req)
|
||||
res = serial.read(1)
|
||||
if res != b"\x06":
|
||||
raise Exception("invalid response")
|
||||
except Exception as e:
|
||||
msg = "Radio refused to enter programming mode: %s" % str(e)
|
||||
raise errors.RadioError(msg)
|
||||
|
||||
|
||||
def exit_programming_mode(radio):
|
||||
serial = radio.pipe
|
||||
|
||||
cmd = b"\x32\x31\x05\xee"
|
||||
req = cmd + bytes([_checksum(cmd)])
|
||||
|
||||
try:
|
||||
# there is no response from this command as the radio resets
|
||||
serial.write(req)
|
||||
except Exception:
|
||||
raise errors.RadioError("Radio refused to exit programming mode")
|
||||
|
||||
|
||||
def _read_block(radio, block_addr):
|
||||
serial = radio.pipe
|
||||
|
||||
cmd = struct.pack(">cH", b"R", block_addr)
|
||||
req = cmd + bytes([_checksum(cmd)])
|
||||
|
||||
try:
|
||||
serial.write(req)
|
||||
res_len = len(cmd) + radio.BLOCK_SIZE + 1
|
||||
res = serial.read(res_len)
|
||||
|
||||
if len(res) != res_len or res[:len(cmd)] != cmd:
|
||||
raise Exception("unexpected reply!")
|
||||
if res[-1] != _checksum(res[:-1]):
|
||||
raise Exception("block failed checksum!")
|
||||
|
||||
block_data = res[len(cmd):-1]
|
||||
except Exception as e:
|
||||
msg = "Failed to read block at %04x: %s" % (block_addr, str(e))
|
||||
raise errors.RadioError(msg)
|
||||
|
||||
return block_data
|
||||
|
||||
|
||||
def _write_block(radio, block_addr, block_data):
|
||||
serial = radio.pipe
|
||||
|
||||
cmd = struct.pack(">cH", b"W", block_addr) + block_data
|
||||
req = cmd + bytes([_checksum(cmd)])
|
||||
|
||||
try:
|
||||
serial.write(req)
|
||||
res = serial.read(1)
|
||||
if res != b"\x06":
|
||||
raise Exception("unexpected reply!")
|
||||
except Exception as e:
|
||||
msg = "Failed to write block at %04x: %s" % (block_addr, str(e))
|
||||
raise errors.RadioError(msg)
|
||||
|
||||
|
||||
def verify_model(radio):
|
||||
# Simply rely on the protocol/checksum to validate the radio model
|
||||
# for now: attempt at least twice, so that garbage in the line is
|
||||
# ignored on the first tries
|
||||
for _ in range(3):
|
||||
try:
|
||||
_read_block(radio, radio.START_ADDR)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
raise errors.RadioError("Could not communicate with the radio")
|
||||
|
||||
|
||||
def do_download(radio):
|
||||
status = chirp_common.Status()
|
||||
status.msg = "Cloning from radio"
|
||||
status.max = radio._memsize
|
||||
|
||||
verify_model(radio)
|
||||
|
||||
data = b""
|
||||
for addr in range(radio.START_ADDR,
|
||||
radio.START_ADDR + radio._memsize,
|
||||
radio.BLOCK_SIZE):
|
||||
status.cur = addr
|
||||
radio.status_fn(status)
|
||||
|
||||
block = _read_block(radio, addr)
|
||||
data += block
|
||||
|
||||
return memmap.MemoryMapBytes(data)
|
||||
|
||||
|
||||
def do_upload(radio):
|
||||
verify_model(radio)
|
||||
enter_programming_mode(radio)
|
||||
|
||||
status = chirp_common.Status()
|
||||
status.msg = "Uploading to radio"
|
||||
status.max = radio._memsize
|
||||
mmap = radio.get_mmap()
|
||||
|
||||
for addr in range(0, radio._memsize, radio.BLOCK_SIZE):
|
||||
status.cur = addr
|
||||
radio.status_fn(status)
|
||||
|
||||
block = mmap[addr:addr + radio.BLOCK_SIZE]
|
||||
_write_block(radio, radio.START_ADDR + addr, block)
|
||||
|
||||
exit_programming_mode(radio)
|
||||
|
||||
|
||||
def mem_to_triplet(mem_tone, mem_code):
|
||||
mem_tone = TONE_LIST[mem_tone]
|
||||
if mem_tone == "Tone":
|
||||
mode = "Tone"
|
||||
code = mem_code / 10
|
||||
polarity = None
|
||||
elif mem_tone in ["DTCS_N", "DTCS_I"]:
|
||||
mode = "DTCS"
|
||||
code = int("%o" % mem_code)
|
||||
polarity = "N" if mem_tone == "DTCS_N" else "R"
|
||||
else:
|
||||
mode = None
|
||||
code = None
|
||||
polarity = None
|
||||
return (mode, code, polarity)
|
||||
|
||||
|
||||
def triplet_to_mem(tone):
|
||||
mode, code, polarity = tone
|
||||
if mode == "Tone":
|
||||
mem_tone = "Tone"
|
||||
mem_code = int(code * 10)
|
||||
elif mode == "DTCS":
|
||||
mem_tone = "DTCS_N" if polarity == "N" else "DTCS_I"
|
||||
mem_code = int('%i' % code, 8)
|
||||
else:
|
||||
mem_tone = ""
|
||||
mem_code = 0
|
||||
mem_tone = TONE_LIST.index(mem_tone)
|
||||
return (mem_tone, mem_code)
|
||||
|
||||
|
||||
@directory.register
|
||||
class KSunM6Radio(chirp_common.CloneModeRadio):
|
||||
VENDOR = "KSUN"
|
||||
MODEL = "M6"
|
||||
BAUD_RATE = 4800
|
||||
NEEDS_COMPAT_SERIAL = False
|
||||
BLOCK_SIZE = 0x10
|
||||
START_ADDR = 0x0050
|
||||
CHANNELS = 80
|
||||
_memsize = BLOCK_SIZE + 10 * CHANNELS
|
||||
|
||||
# Return information about this radio's features, including
|
||||
# how many memories it has, what bands it supports, etc
|
||||
def get_features(self):
|
||||
rf = chirp_common.RadioFeatures()
|
||||
rf.has_bank = False
|
||||
rf.has_name = False
|
||||
rf.has_settings = True
|
||||
rf.memory_bounds = (1, self.CHANNELS)
|
||||
|
||||
rf.can_odd_split = True
|
||||
rf.has_cross = True
|
||||
rf.has_rx_dtcs = True
|
||||
rf.valid_duplexes = ["", "split", "off"]
|
||||
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
|
||||
rf.valid_cross_modes = ["Tone->Tone", "DTCS->", "->DTCS", "Tone->DTCS",
|
||||
"DTCS->Tone", "->Tone", "DTCS->DTCS"]
|
||||
|
||||
rf.has_tuning_step = False
|
||||
rf.has_nostep_tuning = True
|
||||
rf.valid_bands = [(400000000, 480000000)]
|
||||
|
||||
rf.valid_modes = ["FM", "NFM"]
|
||||
rf.valid_power_levels = POWER_LIST
|
||||
|
||||
return rf
|
||||
|
||||
def get_settings(self):
|
||||
settings = self._memobj.settings
|
||||
basic = RadioSettingGroup("basic", "Basic Settings")
|
||||
top = RadioSettings(basic)
|
||||
|
||||
voice = settings.voice
|
||||
rsv = RadioSettingValueList(VOICE_LIST, VOICE_LIST[voice])
|
||||
rs = RadioSetting("voice", "Voice language", rsv)
|
||||
basic.append(rs)
|
||||
|
||||
led = settings.led
|
||||
rsv = RadioSettingValueList(LED_LIST, LED_LIST[led])
|
||||
rs = RadioSetting("led", "LED brighness", rsv)
|
||||
basic.append(rs)
|
||||
|
||||
led_timeout = settings.led_timeout
|
||||
rsv = RadioSettingValueList(LED_TIMEOUT_LIST,
|
||||
LED_TIMEOUT_LIST[led_timeout])
|
||||
rs = RadioSetting("led_timeout", "LED timeout", rsv)
|
||||
basic.append(rs)
|
||||
|
||||
lock_timeout = settings.lock_timeout
|
||||
rsv = RadioSettingValueList(LOCK_TIMEOUT_LIST,
|
||||
LOCK_TIMEOUT_LIST[lock_timeout])
|
||||
rs = RadioSetting("lock_timeout", "Key Lock timeout", rsv)
|
||||
basic.append(rs)
|
||||
|
||||
tot = settings.tot
|
||||
rsv = RadioSettingValueList(TOT_LIST, TOT_LIST[tot])
|
||||
rs = RadioSetting("tot", "Time-Out Timer", rsv)
|
||||
basic.append(rs)
|
||||
|
||||
bat_save = settings.bat_save
|
||||
rsv = RadioSettingValueList(BAT_SAVE_LIST, BAT_SAVE_LIST[bat_save])
|
||||
rs = RadioSetting("bat_save", "Battery Save", rsv)
|
||||
basic.append(rs)
|
||||
|
||||
rsv = RadioSettingValueInteger(0, 9, settings.sq)
|
||||
rs = RadioSetting("sq", "Squelch Level", rsv)
|
||||
basic.append(rs)
|
||||
|
||||
rsv = RadioSettingValueInteger(0, 9, settings.vox)
|
||||
rs = RadioSetting("vox", "VOX Level", rsv)
|
||||
basic.append(rs)
|
||||
|
||||
rsv = RadioSettingValueBoolean(settings.beep)
|
||||
rs = RadioSetting("beep", "Beep", rsv)
|
||||
basic.append(rs)
|
||||
|
||||
channel = settings.channel + 1
|
||||
rsv = RadioSettingValueInteger(1, self.CHANNELS, channel)
|
||||
rs = RadioSetting("channel", "Current Channel", rsv)
|
||||
basic.append(rs)
|
||||
|
||||
return top
|
||||
|
||||
def set_settings(self, settings):
|
||||
settings = settings[0]
|
||||
_settings = self._memobj.settings
|
||||
_settings.voice = VOICE_LIST.index(settings["voice"].value.get_value())
|
||||
_settings.led = LED_LIST.index(settings["led"].value.get_value())
|
||||
_settings.led_timeout = LED_TIMEOUT_LIST.index(
|
||||
settings["led_timeout"].value.get_value())
|
||||
_settings.lock_timeout = LOCK_TIMEOUT_LIST.index(
|
||||
settings["lock_timeout"].value.get_value())
|
||||
_settings.tot = TOT_LIST.index(settings["tot"].value.get_value())
|
||||
_settings.bat_save = BAT_SAVE_LIST.index(
|
||||
settings["bat_save"].value.get_value())
|
||||
_settings.sq = settings["sq"].value.get_value()
|
||||
_settings.vox = settings["vox"].value.get_value()
|
||||
_settings.beep = settings["beep"].value.get_value()
|
||||
_settings.channel = settings["channel"].value.get_value() - 1
|
||||
|
||||
# Do a download of the radio from the serial port
|
||||
def sync_in(self):
|
||||
self._mmap = do_download(self)
|
||||
self.process_mmap()
|
||||
|
||||
# Do an upload of the radio to the serial port
|
||||
def sync_out(self):
|
||||
do_upload(self)
|
||||
|
||||
# Convert the raw byte array into a memory object structure
|
||||
def process_mmap(self):
|
||||
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
|
||||
|
||||
# Return a raw representation of the memory object, which
|
||||
# is very helpful for development
|
||||
def get_raw_memory(self, number):
|
||||
return repr(self._memobj.memory[number-1])
|
||||
|
||||
# Extract a high-level memory object from the low-level memory map
|
||||
# This is called to populate a memory in the UI
|
||||
def get_memory(self, number):
|
||||
_mem = self._memobj.memory[number-1]
|
||||
|
||||
mem = chirp_common.Memory()
|
||||
mem.number = number
|
||||
|
||||
if _mem.freq.get_raw() == bytes([255] * 5):
|
||||
mem.empty = True
|
||||
else:
|
||||
rx_freq = ((_mem.freq[0] << 12)
|
||||
+ (_mem.freq[1] << 4) + (_mem.freq[2] >> 4))
|
||||
tx_freq = (((_mem.freq[2] & 0xF) << 16)
|
||||
+ (_mem.freq[3] << 8) + _mem.freq[4])
|
||||
|
||||
mem.freq = rx_freq * 1000000 // 2000
|
||||
mem.offset = tx_freq * 1000000 // 2000
|
||||
|
||||
if _mem.no_tx:
|
||||
mem.duplex = "off"
|
||||
elif rx_freq != tx_freq:
|
||||
mem.duplex = "split"
|
||||
|
||||
chirp_common.split_tone_decode(
|
||||
mem,
|
||||
mem_to_triplet(_mem.tx_tone, _mem.tx_code),
|
||||
mem_to_triplet(_mem.rx_tone, _mem.rx_code))
|
||||
|
||||
mem.mode = "NFM" if not mem.empty and _mem.nfm else "FM"
|
||||
mem.power = POWER_LIST[int(not mem.empty and _mem.low_pwr)]
|
||||
mem.skip = "S" if not mem.empty and _mem.skip else ""
|
||||
|
||||
mem.extra = RadioSettingGroup("Extra", "extra")
|
||||
|
||||
hopping = False if mem.empty else _mem.hopping
|
||||
rsv = RadioSettingValueBoolean(hopping)
|
||||
rs = RadioSetting("hopping", "Hopping", rsv)
|
||||
mem.extra.append(rs)
|
||||
|
||||
compander = False if mem.empty else _mem.compander
|
||||
rsv = RadioSettingValueBoolean(compander)
|
||||
rs = RadioSetting("compander", "Compander", rsv)
|
||||
mem.extra.append(rs)
|
||||
|
||||
scrambler = False if mem.empty else _mem.scrambler
|
||||
rsv = RadioSettingValueList(SCRAMBLER_LIST, SCRAMBLER_LIST[scrambler])
|
||||
rs = RadioSetting("scrambler", _("Scrambler"), rsv)
|
||||
mem.extra.append(rs)
|
||||
|
||||
return mem
|
||||
|
||||
# Store details about a high-level memory to the memory map
|
||||
# This is called when a user edits a memory in the UI
|
||||
def set_memory(self, mem):
|
||||
_mem = self._memobj.memory[mem.number-1]
|
||||
|
||||
if mem.empty:
|
||||
_mem.fill_raw(b"\xff")
|
||||
else:
|
||||
rx_freq = mem.freq
|
||||
if mem.duplex == "split" and mem.offset:
|
||||
tx_freq = mem.offset
|
||||
else:
|
||||
tx_freq = rx_freq
|
||||
|
||||
rx_freq = round(rx_freq / 1000000 * 2000)
|
||||
tx_freq = round(tx_freq / 1000000 * 2000)
|
||||
|
||||
_mem.freq[0] = (rx_freq >> 12) & 0xff
|
||||
_mem.freq[1] = (rx_freq >> 4) & 0xff
|
||||
_mem.freq[2] = ((rx_freq << 4) & 0xf0) | ((tx_freq >> 16) & 0x0f)
|
||||
_mem.freq[3] = (tx_freq >> 8) & 0xff
|
||||
_mem.freq[4] = (tx_freq) & 0xff
|
||||
|
||||
tx_tone, rx_tone = chirp_common.split_tone_encode(mem)
|
||||
_mem.tx_tone, _mem.tx_code = triplet_to_mem(tx_tone)
|
||||
_mem.rx_tone, _mem.rx_code = triplet_to_mem(rx_tone)
|
||||
|
||||
_mem.no_tx = (mem.duplex == "off")
|
||||
_mem.nfm = (mem.mode == "NFM")
|
||||
_mem.skip = (mem.skip == "S")
|
||||
|
||||
if mem.power in POWER_LIST:
|
||||
_mem.low_pwr = POWER_LIST.index(mem.power)
|
||||
else:
|
||||
_mem.low_pwr = False
|
||||
|
||||
if "hopping" in mem.extra:
|
||||
_mem.hopping = mem.extra["hopping"].value.get_value()
|
||||
else:
|
||||
_mem.hopping = False
|
||||
|
||||
if "compander" in mem.extra:
|
||||
_mem.compander = mem.extra["compander"].value.get_value()
|
||||
else:
|
||||
_mem.compander = False
|
||||
|
||||
if "scrambler" in mem.extra:
|
||||
_mem.scrambler = SCRAMBLER_LIST.index(
|
||||
mem.extra["scrambler"].value.get_value())
|
||||
else:
|
||||
_mem.scrambler = 0
|
||||
|
|
@ -151,6 +151,7 @@
|
|||
| <a name="Jetstream_JT270M"></a> Jetstream_JT270M | [Implied by Jetstream_JT270M](#user-content-Jetstream_JT270M) | 13-Dec-2022 | Yes | 0.01% |
|
||||
| <a name="Jetstream_JT270MH"></a> Jetstream_JT270MH | [@kk7ds](https://github.com/kk7ds) | 13-Dec-2022 | Yes | 0.01% |
|
||||
| <a name="Jianpai_8800_Plus"></a> Jianpai_8800_Plus | [Implied by Radtel_RT-490](#user-content-Radtel_RT-490) | 11-Nov-2023 | Yes | 0.01% |
|
||||
| <a name="KSUN_M6"></a> KSUN_M6 | | | Yes | |
|
||||
| <a name="KYD_IP-620"></a> KYD_IP-620 | | | | 0.02% |
|
||||
| <a name="KYD_NC-630A"></a> KYD_NC-630A | | | | 0.02% |
|
||||
| <a name="Kenwood_HMK"></a> Kenwood_HMK | [Implied by Generic_CSV](#user-content-Generic_CSV) | 4-Dec-2022 | | 0.00% |
|
||||
|
|
@ -461,11 +462,11 @@
|
|||
| <a name="Zastone_ZT-X6"></a> Zastone_ZT-X6 | [Implied by Retevis_RT22](#user-content-Retevis_RT22) | 9-Dec-2022 | Yes | 0.11% |
|
||||
## Stats
|
||||
|
||||
**Drivers:** 458
|
||||
**Drivers:** 459
|
||||
|
||||
**Tested:** 87% (402/56) (93% of usage stats)
|
||||
**Tested:** 87% (402/57) (93% of usage stats)
|
||||
|
||||
**Byte clean:** 91% (419/39)
|
||||
**Byte clean:** 91% (420/39)
|
||||
|
||||
## Meaning of this testing
|
||||
|
||||
|
|
|
|||
BIN
tests/images/KSUN_M6.img
Normal file
BIN
tests/images/KSUN_M6.img
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue