Skip to content

Command Interface Module

cmdif - Command Interface Processor.

This module implements a command interface for receiving commands from a console, processing, the command, and printing results. Commands are implemented in a standardized way (see CommandTemplate) to make it "easy" to add additional commands, sort of like a plugin. Any command implementation can schedule itself to run once, repeatedly, or with periodic timing.

Most commands are added from their own modules, but there are some built-in commands here that are used to support all commands:

  • CmdHelp - provide a basic help function
  • CmdConfig - provide a way to pass configuration to any command module
  • CmdAdd - provide a way to add new command module to the list of commands

This module relies on the presence of the console_std module which provides an abstraction of read and write functions for a console. This should allow this module to be used with different mechanisms of input and output.

CmdAdd

Bases: CommandTemplate

Provide a command that allows adding new commands.

Any command classes that are implemented and listed in cmdclasses.py can be added to the command list. This allows for the existence of many kinds of commands (mostly LED patterns) when only some will be used for a particular installation. Common firmware can be installed on multiple controllers, and the add command in combination with the config command allows a particular controller to be configured at run time from the console command line.

Source code in ledstrip/cmdif.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
class CmdAdd(CommandTemplate):
    """Provide a command that allows adding new commands.

    Any command classes that are implemented and listed in
    [`cmdclasses.py`][ledstrip.cmdclasses] can be added to the command list.
    This allows for the existence of many kinds of commands (mostly LED
    patterns) when only some will be used for a particular installation. Common
    firmware can be installed on multiple controllers, and the `add` command in
    combination with the `config` command allows a particular controller to be
    configured at run time from the console command line.
    """
    helpstr = "Add new command (add,newname,ClassName)"

    # provide the cmdinterface so it can add commands
    def __init__(self, cmdinterface):
        super().__init__()
        self._ci = cmdinterface

    # TODO add error handling to below

    # this is called when parm[0]=='add'
    # parm[1] should be the name of the command (how it will be invoked)
    # parm[2] is the Class name that implements the command (from cmdclasses.py)
    def render(self, parmlist, framebuf):
        if len(parmlist) == 3:
            cmdname = parmlist[1]
            clsname = parmlist[2]
            cmdobj = globals()[clsname]()
            self._ci.add_cmd(cmdname, cmdobj)
        return None

CmdConfig

Bases: CommandTemplate

Provide a configuration command, to configure other commands.

Some commands have configuration options. This command class provides a standard way to set configuration options for a command.

It is invoked as config,<cmdname>,parm1,parm2,...

The parameters are passed through to the specified command's config handler if it has one. No checking is done on the actual parameters.

Source code in ledstrip/cmdif.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class CmdConfig(CommandTemplate):
    """Provide a configuration command, to configure other commands.

    Some commands have configuration options. This command class provides a
    standard way to set configuration options for a command.

    It is invoked as `config,<cmdname>,parm1,parm2,...`

    The parameters are passed through to the specified command's config handler
    if it has one. No checking is done on the actual parameters.
    """
    helpstr = "config,<cmdname>,parm1,parm2,..."

    def __init__(self, cmddict):
        super().__init__()
        self._dict = cmddict

    # this is called when parm[0]=='config'
    # parm[1] should be the command to be conigured
    # parm[2] and greater are configuration parameters, which vary depending
    # on the command that is being configured
    def render(self, parmlist, framebuf):
        # check that there at least one parameter, and that the specified
        # command exists, and then pass to the command's config handler.
        if len(parmlist) >= 3:
            cmdname = parmlist[1]
            if cmdname in self._dict:
                cmdobj = self._dict[cmdname]
                cmdobj.config(parmlist)
        return None

CmdHelp

Bases: CommandTemplate

Provide a basic help command.

This command class provides two commands to show help to the user. It uses strings that are part of the CommandTemplate as the help message.

The two commands are:

  • help - show simple help message for every installed command
  • help,config - show configuration options for a command, if any

The help is automatic for each command as long as the implementation provides a help string.

Source code in ledstrip/cmdif.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class CmdHelp(CommandTemplate):
    """Provide a basic help command.

    This command class provides two commands to show help to the user. It uses
    strings that are part of the CommandTemplate as the help message.

    The two commands are:

    * `help` - show simple help message for every installed command
    * `help,config` - show configuration options for a command, if any

    The help is automatic for each command as long as the implementation
    provides a help string.
    """
    helpstr = "show list of commands"

    def __init__(self, cmddict):
        super().__init__()
        self._dict = cmddict

    def cmdhelp(self):
        """Show basic help messages to the user."""
        console_writeln("\nCommands")
        console_writeln("--------")
        for cmdname, cmdobj in self._dict.items():
            cmdhelp = cmdobj.helpstr
            console_writeln(f"{cmdname:<8} : {cmdhelp}")
        console_writeln("")

    def cfghelp(self):
        """Show configuration help to the user."""
        console_writeln("\nConfigs")
        console_writeln("-------")
        for cmdname, cmdobj in self._dict.items():
            cfghelp = cmdobj.cfgstr
            console_writeln(f"{cmdname:<8} : {cfghelp}")
        console_writeln("")

    # this is called whenever parm[0]=='help'
    def render(self, parmlist, framebuf):
        # decide if this is a regular help, or a config help, and then
        # call the appropriate method to show the help to the user
        if len(parmlist) == 2 and parmlist[1] == "config":
            self.cfghelp()
        else:
            self.cmdhelp()
        return None

cfghelp()

Show configuration help to the user.

Source code in ledstrip/cmdif.py
78
79
80
81
82
83
84
85
def cfghelp(self):
    """Show configuration help to the user."""
    console_writeln("\nConfigs")
    console_writeln("-------")
    for cmdname, cmdobj in self._dict.items():
        cfghelp = cmdobj.cfgstr
        console_writeln(f"{cmdname:<8} : {cfghelp}")
    console_writeln("")

cmdhelp()

Show basic help messages to the user.

Source code in ledstrip/cmdif.py
69
70
71
72
73
74
75
76
def cmdhelp(self):
    """Show basic help messages to the user."""
    console_writeln("\nCommands")
    console_writeln("--------")
    for cmdname, cmdobj in self._dict.items():
        cmdhelp = cmdobj.helpstr
        console_writeln(f"{cmdname:<8} : {cmdhelp}")
    console_writeln("")

CmdInterface

Provides methods for processing command line input.

Source code in ledstrip/cmdif.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
class CmdInterface():
    """Provides methods for processing command line input."""
    def __init__(self, framebuf=None):
        self._cmds = OrderedDict()
        # dictionary format:
        # key - command name as string
        # value - CommandPattern object
        self._cmds["help"] = CmdHelp(self._cmds)
        self._cmds["config"] = CmdConfig(self._cmds)
        self._cmds["add"] = CmdAdd(self)

        # temporary additional commands
        self._cmds["range"] = LedRange()
        self._cmds["meter"] = LedMeter()
        self._cmds["randomog"] = LedRandomOG()
        self._cmds["random"] = LedRandom()
        self._cmds["stop"] = LedStop()
        #

        self._cp = cmdparser.CmdParser()
        self._framebuf = framebuf
        self._cmdobj = None
        self._cmdparms = None
        self._sched = 0
        console_init()

    def add_cmd(self, cmdname: str, cmdobj: CommandTemplate) -> None:
        """Adds a new command of the specified class to the command list.

        :param cmdname: name of the new command, must be unique from other
            command names
        :param cmdobj: a [CommandTemplate][ledstrip.cmdtemplate] subclass
            implementing the new command
        """
        self._cmds[cmdname] = cmdobj

    def setup(self, param_list: list[str]) -> None:
        """Setup to start running a new command.

        This is called by the command loop whenever a complete command line is
        received. It uses the input parameter list to determine if the named
        command exists. If so it sets up for executing in the exec loop.

        It send `$OK` or `$ERR` to the serial console as a reply. Any currently
        running command will be canceled, even if the new command is invalid.

        :param param_list: string list of all the command line parameters,
            including the command name which is the first item.
        """
        if param_list[0] in self._cmds:
            # if new command is valid, schedule it to run immediately
            self._cmdobj = self._cmds[param_list[0]]
            self._cmdparms = param_list
            self._sched = time.ticks_us()
            console_writeln("$OK")
        else:
            # if command is not valid, cancel anything in progress and
            # indicate error to console
            self._cmdobj = None
            console_writeln("$ERR")

    def exec(self) -> bool:
        """Execute currently scheduled command.

        This method is called repeatedly from the run loop. It checks to see
        if any command is scheduled to run, and if so calls the `render()`
        method for that command. The return value determines how the command
        is rescheduled. The return value is:

        * None - do not run again
        * int(0) - run again immediately
        * int(N) > 0 - run after N ticks have elapsed, in microseconds

        The return value is `True` if the command was run, or `False` if not.

        **NOTES:** if render() was called, the `True` return will cause the
        LED strip to be repainted, even if the frame buffer was not updated.
        Some commands do not even affect the LED status. In the future consider
        a way for render() itself to indicate if a repaint is needed.
        """
        # check for scheduled command
        if self._cmdobj:
            # get the current time and compare to scheduled time
            now = time.ticks_us()
            if time.ticks_diff(now, self._sched) >= 0:
                # time has elapsed so run the command
                delay = self._cmdobj.render(self._cmdparms, self._framebuf)
                # process the return from the command ...
                if delay is None:
                    self._cmdobj = None  # doesnt need to run again
                elif delay == 0:
                    self._sched = now    # run again immediately
                else:
                    # schedule next run time
                    self._sched = time.ticks_add(now, delay)
                return True     # need to repaint

        # frame buffer was not updated so no need to repaint
        return False

    def run(self) -> bool:
        """Command line processing and run loop.

        This method is called repeatedly from the top level run loop. It
        processes incoming data from the command line, calls the parser, and
        dispatches commands when a complete command line is received.

        It returns whatever `exec()` returns, which is used as a repaint signal
        at the top level.
        """
        # process any new incoming characters
        incoming = console_read()
        if incoming:
            console_write(incoming)     # echo to console
            cmdargs = self._cp.process_input(incoming)

            # if there is a complete new command line, then setup new command
            if cmdargs:
                self.setup(cmdargs)

        # run existing scheduled command
        return self.exec()

add_cmd(cmdname: str, cmdobj: CommandTemplate) -> None

Adds a new command of the specified class to the command list.

Parameters:

Name Type Description Default
cmdname str

name of the new command, must be unique from other command names

required
cmdobj CommandTemplate

a CommandTemplate subclass implementing the new command

required
Source code in ledstrip/cmdif.py
186
187
188
189
190
191
192
193
194
def add_cmd(self, cmdname: str, cmdobj: CommandTemplate) -> None:
    """Adds a new command of the specified class to the command list.

    :param cmdname: name of the new command, must be unique from other
        command names
    :param cmdobj: a [CommandTemplate][ledstrip.cmdtemplate] subclass
        implementing the new command
    """
    self._cmds[cmdname] = cmdobj

exec() -> bool

Execute currently scheduled command.

This method is called repeatedly from the run loop. It checks to see if any command is scheduled to run, and if so calls the render() method for that command. The return value determines how the command is rescheduled. The return value is:

  • None - do not run again
  • int(0) - run again immediately
  • int(N) > 0 - run after N ticks have elapsed, in microseconds

The return value is True if the command was run, or False if not.

NOTES: if render() was called, the True return will cause the LED strip to be repainted, even if the frame buffer was not updated. Some commands do not even affect the LED status. In the future consider a way for render() itself to indicate if a repaint is needed.

Source code in ledstrip/cmdif.py
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def exec(self) -> bool:
    """Execute currently scheduled command.

    This method is called repeatedly from the run loop. It checks to see
    if any command is scheduled to run, and if so calls the `render()`
    method for that command. The return value determines how the command
    is rescheduled. The return value is:

    * None - do not run again
    * int(0) - run again immediately
    * int(N) > 0 - run after N ticks have elapsed, in microseconds

    The return value is `True` if the command was run, or `False` if not.

    **NOTES:** if render() was called, the `True` return will cause the
    LED strip to be repainted, even if the frame buffer was not updated.
    Some commands do not even affect the LED status. In the future consider
    a way for render() itself to indicate if a repaint is needed.
    """
    # check for scheduled command
    if self._cmdobj:
        # get the current time and compare to scheduled time
        now = time.ticks_us()
        if time.ticks_diff(now, self._sched) >= 0:
            # time has elapsed so run the command
            delay = self._cmdobj.render(self._cmdparms, self._framebuf)
            # process the return from the command ...
            if delay is None:
                self._cmdobj = None  # doesnt need to run again
            elif delay == 0:
                self._sched = now    # run again immediately
            else:
                # schedule next run time
                self._sched = time.ticks_add(now, delay)
            return True     # need to repaint

    # frame buffer was not updated so no need to repaint
    return False

run() -> bool

Command line processing and run loop.

This method is called repeatedly from the top level run loop. It processes incoming data from the command line, calls the parser, and dispatches commands when a complete command line is received.

It returns whatever exec() returns, which is used as a repaint signal at the top level.

Source code in ledstrip/cmdif.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def run(self) -> bool:
    """Command line processing and run loop.

    This method is called repeatedly from the top level run loop. It
    processes incoming data from the command line, calls the parser, and
    dispatches commands when a complete command line is received.

    It returns whatever `exec()` returns, which is used as a repaint signal
    at the top level.
    """
    # process any new incoming characters
    incoming = console_read()
    if incoming:
        console_write(incoming)     # echo to console
        cmdargs = self._cp.process_input(incoming)

        # if there is a complete new command line, then setup new command
        if cmdargs:
            self.setup(cmdargs)

    # run existing scheduled command
    return self.exec()

setup(param_list: list[str]) -> None

Setup to start running a new command.

This is called by the command loop whenever a complete command line is received. It uses the input parameter list to determine if the named command exists. If so it sets up for executing in the exec loop.

It send $OK or $ERR to the serial console as a reply. Any currently running command will be canceled, even if the new command is invalid.

Parameters:

Name Type Description Default
param_list list[str]

string list of all the command line parameters, including the command name which is the first item.

required
Source code in ledstrip/cmdif.py
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
def setup(self, param_list: list[str]) -> None:
    """Setup to start running a new command.

    This is called by the command loop whenever a complete command line is
    received. It uses the input parameter list to determine if the named
    command exists. If so it sets up for executing in the exec loop.

    It send `$OK` or `$ERR` to the serial console as a reply. Any currently
    running command will be canceled, even if the new command is invalid.

    :param param_list: string list of all the command line parameters,
        including the command name which is the first item.
    """
    if param_list[0] in self._cmds:
        # if new command is valid, schedule it to run immediately
        self._cmdobj = self._cmds[param_list[0]]
        self._cmdparms = param_list
        self._sched = time.ticks_us()
        console_writeln("$OK")
    else:
        # if command is not valid, cancel anything in progress and
        # indicate error to console
        self._cmdobj = None
        console_writeln("$ERR")