Skip to content

Add new RadioSettingValue Classes for Hex and DTMF Values - #1558

Open
TrimbleSoftware wants to merge 18 commits into
kk7ds:masterfrom
TrimbleSoftware:master
Open

TrimbleSoftware wants to merge 18 commits into
kk7ds:masterfrom
TrimbleSoftware:master

Conversation

@TrimbleSoftware

Copy link
Copy Markdown
Contributor

Add new RadioSettingValue Classes and for Hex and DTMF Values

The inspiration for this enhancement comes from a recent driver change that added a Hex value column to the memory grid and there were problems getting the value validation methods to work properly without causing unhandled exceptions.

This addition to the CHIRP UI functionality allows a consistent and easy approach to entering, editing, formatting and validating Hex and DTMF values. These new input fields work just like all the other RadioSettingValueX classes and accept values to assist in validation of input and formatting current values. GUI feedback is provided for values that don't pass validation rules and values.

It has always been a chore to add validation and formatting code when adding Hex or DTMF values to a CHIRP driver for mem.exrta columns or settings. Especially when using a string input then having to format, parse and validate the input.

There are many drivers that have had to deal with the rigors of entering, formatting and validation Hex and DTMF values. Each one has always had to come up with some novel way to handle this reoccurring need. And as such, the result and presentation of this type of information can and will differ.

The new added Classes are:

RadioSettingValueHex
Only accepts input of valid hex values (0-9, A-F) in either upper or lower case or with a 0X or 0x prefix. Both min and max values and min and max length and step can be specified. All output is consistently formatted with a 0x prefix and all hex digits are converted to uppercase.

This new input field can be used for memory columns or settings with Hex values like FHSS Codes or APRS values and the like. Use it anywhere it makes sense to prompt for a value in Hex notation.

RadioSettingValueDTMF
Only accepts input of valid DTMF character values (0-9, A-D, #,*) in either upper of lower case. Both a min and max length can be specified. All output is consistently formatted with all DTMF characters converted to uppercase.

This new input field can be used for memory columns or settings with DTMF values like PTT IDs or DTMF code values. Use it anywhere it makes sense to prompt for a literal DTMF code string value.

Usage:
The new Classes have to be imported to used:

from chirp.settings import (
    ...
    RadioSettingValueHex,
    RadioSettingValueDTMF,
    ...
)

And a validated mem.extra column can be created with:

        ...
        rs = RadioSettingValueHex(
            0, 6, 0, 0x7fffff, _code_to_hex(_mem.code), 1)
        rset = RadioSetting("code", "FHSS Code", rs)
        rset.set_doc("Manual FHSS Code in Hex (0x000000-0x7FFFFF,"
                     " 0-6 chars)")
        mem.extra.append(rset)
        ...

For a validated Setting value here is a somewhat more complex example that prompts for a series of 15 DTMF values:

        ...
        for i in range(0, 15):
            _codeobj = self._memobj.pttid[i].code
            _code = "".join([DTMF_CHARS[x] for x in _codeobj if int(x) < 0x1F])
            rs = RadioSettingValueDTMF(0, len(_codeobj), _code)
            rset = RadioSetting("pttid/%i.code" % i,
                                "PTT-ID Code %i" % (i + 1), rs)
            rset.set_apply_callback(apply_code, self._memobj.pttid[i],
                                    len(_codeobj))
            rset.set_doc("DTMF PTT-ID Code %i (0-%i chars)" % (i + 1,
                                                               len(_codeobj)))
            dtmf.append(rset)
        ...

@kk7ds kk7ds left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, this is a great idea, thanks for thinking about "the system" instead of just a single driver. Obviously there's a lot more bespoke validation in a lot of places than would be ideal, so standardizing these sorts of things is very good. We can probably clean up some of that from a bunch of drivers once this is in.

That, said I think this exact implementation mixes too much of the view and the model, but it's definitely on the right track. If you look lower in settings.py you'll see MemSetting, which I really want to see more people using instead of the sort of old way of manually applying settings to memory object values (I need to write some docs about that). That method can basically walk a tree of setting values and set them on the memory object, pretty much doing the entire set_settings() function on its own in a generic way for most drivers. However, it needs the value models to behave like it expects, and for something like the Hex value you have here - it needs to behave more like an integer than a string (because that's what it is).

We already have a hex-enforcing UI element in the radio browser, but.. can I write you the equivalent for a RadioSetting and we re-swizzle this to let the UI do its job of controlling the way the data is viewed/entered and make your model classes here just do the model part?

Again, thanks very much for working on this as a holistic solution!

Comment thread chirp/wxui/common.py Outdated
elif isinstance(value, settings.RadioSettingValueHex):
editor = self._get_editor_str(element, value)
elif isinstance(value, settings.RadioSettingValueDTMF):
editor = self._get_editor_str(element, value)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isinstance() actually takes a tuple of valid classes, so you could define things_that_edit_like_strings = (RadioSettingValueString, RadioSettingValueHex, ...)

(just a comment)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good catch. It would consolidate the code somewhat.

Comment thread chirp/settings.py Outdated
try:
if str(value).strip():
value = int(value, 16)
except Exception:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should catch the actual things we expect (which is probably just ValueError right?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either-Or. But ValueError is probably more approiate.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always best to catch the actual things you expect to avoid squashing something else that happened into "you entered an invalid digit". It's easy to always catch Exception, but it's not usually best. Some projects have static checkers that won't let you catch Exception unless you're also handling the expected ones separately.

Comment thread chirp/settings.py Outdated
self._minlength = minlength
self._maxlength = maxlength
self._min = minval
self._max = maxval

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a bit strange to have a min/maxlength and a min/maxval as they sorta overlap but not quite. Isn't it enough to have just maxval? Also, min/maxlength - do they include the prefix or not? You're basically controlling the display/view of the data in here (which is the model) and overriding the UI's job.

For hex, it makes more sense to me to make this subclass from IntegerValue and only override the UI element for editing/display since the underlying thing is still just an integer in memory. Then, the prefix part is just an artifact of the display and we can just enforce the min/maxval like normal, right? Like, the UI could enforce that the 0x prefix never leaves the editor field, or is displayed in a different color or something. Having the SettingValue here (the model) keep forcing it back in will make it harder to do that.

I think if you just subclass the integer class for a hex one, with no other difference, then the UI can know whether or not to use a Hex editor class or not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I wrote this I was debating that very thing. Then I thought of the use-case where you might need a 4 digit hex number between 0xF and 0xFFFF for example. Then you would need both min and max value as well as min and max length to validate against. In this case it would avoid having to input 0x000F to get 0xF. After the input it would be displayed as 0x000F.

Internally it is treated as a binary integer and is only formatted as a string on output. It would be great if the UI knew how and when to do that to display the value in an industry standard Hex notation.

The "0x" prefix is specifically excluded from the min and max length validation values.

That is probably a good idea to subclass the IntegerValue and override the needed functionality.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the UI is best positioned to handle the display and input part. Let it reject invalid keystrokes but not obsess over the length until the user tries to save the value, then reformat to four characters with prefix, etc.

Comment thread chirp/settings.py Outdated
return self._step


class RadioSettingValueDTMF(RadioSettingValue):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this just a string with a CHARSET = CHARSET_DTMF?

I think using that will be almost exactly the same except it won't have "DTMF" in the error messages. Could we just subclass StringValue and pass our CHARSET to it. Could maybe even get similar error behavior by wrapping set_value(). Something like:

class RadioSettingValueDTMF(RadioSettingValueString):
    def __init__(self, minlength, maxlength, current):
        super().__init__(minlength, laxlength, current, charset=CHARSET_DTMF)

    def set_value(self, value):
        try:
            super.set_value(value)
        except InvalidValueError as e:
            raise InvalidValueError('%s (DTMF values must be digits: %s)' % (e, CHARSET_DTMF)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That may work to simplify the code. I'd still want to normalize the output (like accept lowercase input, but convert it to uppercase for display consistancey, but here again a model vs. view argument...).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, let the UI handle that part. The RadioSettingValue is just the carrier of the actual value, which is always an integer and thus has no min/maxlength.

So maybe give me a bit (possibly later today) and I'll see if I can cook up the UI bit and trim the display bits of this part down to give you something to play with.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great!

@TrimbleSoftware

Copy link
Copy Markdown
Contributor Author

That, said I think this exact implementation mixes too much of the view and the model, but it's definitely on the right track. If you look lower in settings.py you'll see MemSetting, which I really want to see more people using instead of the sort of old way of manually applying settings to memory object values (I need to write some docs about that). That method can basically walk a tree of setting values and set them on the memory object, pretty much doing the entire set_settings() function on its own in a generic way for most drivers. However, it needs the value models to behave like it expects, and for something like the Hex value you have here - it needs to behave more like an integer than a string (because that's what it is).

I use and like MemSetting. I use it whenever possible, however I've never been able to get to to use apply callbacks. I've always had to revert to using RadioSetting for that. May be just something I'm doing wrong too. The MemSetting docs would be a good idea for sure.

@kk7ds

kk7ds commented Apr 19, 2026

Copy link
Copy Markdown
Owner

I use and like MemSetting. I use it whenever possible, however I've never been able to get to to use apply callbacks. I've always had to revert to using RadioSetting for that. May be just something I'm doing wrong too. The MemSetting docs would be a good idea for sure.

Well, the point of MemSetting is somewhat to avoid the need for callbacks.. if it supported a callback apply function, it would work differently than the current one, since the "how to set this value" is assumed. The likely use case is if you are doing validation in the apply function (which ideally you wouldn't, but not always possible of course). Perhaps separating the validation into a callback for it would be a better split.

Added this to at least encourage people to use it for new code.

@kk7ds

kk7ds commented Apr 19, 2026

Copy link
Copy Markdown
Owner

Here's something to try with the Hex value. I added a setting to FakeLiveRadio so you can "download" from that and have something to poke at. It keeps the required 0x prefix and limits keystrokes to just hex values. Let me know what you think and I can work on the DTMF one next.

@TrimbleSoftware

TrimbleSoftware commented Apr 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Here's something to try with the Hex value. I added a setting to FakeLiveRadio so you can "download" from that and have something to poke at. It keeps the required 0x prefix and limits keystrokes to just hex values. Let me know what you think and I can work on the DTMF one next.

I tried out the your RadioSettingValueHex and it works pretty good. This definitely the premier way to implement specific inputs like Hex and DTMF fields. At least the model-tail-is-not-wagging-the-view-dog now! LOL

I noticed these glitches:

  1. When setting focus (tabbing to or clicking) to the field, the value will highlight, but you can't overtype it like other input fields in CHIRP. You have to backspace one character at a time to replace the whole value.
  2. Tried it in mem.extra and of course it acts just like an integer input filed. No hex display or allowing ABCDEF chars.

This is gelled enough now for me to ask you to work on the DTMF editor too...

Thanks for entertaining this kind of a fix for CHIRP,

Fred

@kk7ds

kk7ds commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Sorry for the delay here, busy week. I'll circle back next week and work on those things and on DTMF. Thanks!

@TrimbleSoftware

TrimbleSoftware commented Apr 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Sorry for the delay here, busy week. I'll circle back next week and work on those things and on DTMF. Thanks!

I took a WAG at fixing the HexText by hooking the keyboard and mouse focus events and selection just the right amount of the hex value. Seems to work OK on the fake live driver. One problem that still exists is that HexText will not allow a blank/null value. 0x00 is not the same as a NULL value. Usually radios allow a null hex value (all 0xFF values).

Also took another WAG at adding the DTMFText editor as a subclass of string with the charset fixed and the padchar removed. It had some of the same focus problems as the HexText and were fixed the same way.

Also mem.extra is something I have no clue an adding these new editors to.

@kk7ds

kk7ds commented Apr 30, 2026

Copy link
Copy Markdown
Owner

I haven't forgotten about this I just know it's not super critical and have had a lot else going on. Thanks for your patience :)

@kk7ds kk7ds left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the updates, this mostly works the way I expect. Lemme have the "lock" on this for a bit if I can and I'll try to make the cleanups and changes I've identified here and squash things down appropriately.

Comment thread chirp/wxui/common.py
# Arrow keys and navigation allowed anywhere
pos = self._text.GetSelection()[1] # get the right pos

if key in self.SHIFTED_TO_INGNORE and event.ShiftDown():

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to understand the logic here. What you want to do is only allow shift to be held for the letter digits right? I think it would be more straightforward to put this down in the clause that handles those instead of this pre-filter before any of the other logic. Also, we can use chr(key).isdigit() to handle that and make it a little more obvious what's going on I think.

@TrimbleSoftware TrimbleSoftware May 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my Win11 dev laptop, the top row of number keys 1,2,3, etc. returns the same key code whether the shift key is pressed of not. So with out that conditional it was allowing the shifted value of the key to be entered into the field. For example it the Shift+4 was typed a '$' would be entered into the field. Which is not an acceptable value for Hex or DTMF text. That is why there is separate ignore lists for Hex and DTMF. (DTMF includes # and *, Hex does not)

I'm sure there would be a simpler way to handle this logic...

Comment thread chirp/wxui/common.py
self._text.SetSelection(2, -1) # move beyond the "0x"

def _on_focus(self, event):
self._text.Enable(True)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would un-disable a field if we had it disabled for some reason. Is this left over from experimentation or here for some reason?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an attempt to keep the "0x" part of the HexText from being selected when the editor get focus. Otherwise that prefix gets selected and is subject to overtyping when editing the int value.

Comment thread chirp/wxui/common.py
wx.CallAfter(self._select_text)
event.Skip()

def _on_char(self, event):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's probably better to do this in KEY_DOWN if we can to avoid catching both of those. Did you try changing the key in the event by chance before you went this route? I can give it a shot.

Comment thread chirp/wxui/common.py
return


class DTMFText(HexText):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll also try to refactor the base behaviors to include an optional prefix and let Hex and DTMF inherit from that so we don't imply that DTMF is a special case of Hex. And, I think we can avoid a bunch of the duplication below if we make it general.

Who knows, maybe we'll need an OctalText at some point.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OctalText, maybe... :)

What I could use now is a way for a driver to ask the UI to prompt for a radio programming password. The UI would the have to pass the collected value back to the driver so the value could be used in the enter_programming() phase of the handshake. So something like a PasswordText editor field or a call to wx.PasswordEntryDialog could be used to prompt for the password value. I'm not sure how the UI could then pass that value to the driver.

https://docs.wxpython.org/wx.PasswordEntryDialog.html

Comment thread chirp/wxui/common.py

class HexText(wx.propgrid.PGTextCtrlEditor):
HEX_CODES = [ord(x) for x in '0123456789abcdefABCDEF']
SHIFTED_TO_INGNORE = [ord(x) for x in '1234567890']

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is mispelled :)

@TrimbleSoftware

TrimbleSoftware commented May 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for the updates, this mostly works the way I expect. Lemme have the "lock" on this for a bit if I can and I'll try to make the cleanups and changes I've identified here and squash things down appropriately.

You've got the Conn!

@kk7ds

kk7ds commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Sorry, I've let this languish too long. If you can fix the typos and squash let's just not hold this up any further. If you could drop comments on the obvious things (like the KEY_DOWN suggestion above for posterity) that would be great. Otherwise let's just clear the plate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants