Overview
The MyState library delivers a framework to simplify configuration and control of appliance-like devices. MyState makes it quick-and-easy to define device state that is readily controllable from the outside world.
Writing code to control state and react to sensor inputs is time consuming. Why not (mostly) solve the problem once, and re-use that infrastructure to build up your next project faster?
Features
- Route raw sensed input signals through custom signal-generating filters, and provide a solid, uniform user-interface experience.
- Load custom device configuration/controlled state on startup by calling
ListenerRoot.script_load().- A good way to "recall presets" at the press of a button when manual configuration is a bit of a pain.
- Let anyone control your device using
SigLinkinterface (by means of a serial/other IO connection).- Ex: Let a PC control your device using
MyState"signals" sent across the USB/serial connection. - Configure your device using a custom-built GUI or web interface.
- Ex: Let a PC control your device using
- Applies more formalized (hopefully readable) patterns of "filtering" input/sense signals, and applying a "reactive paradigm" to respond accordingly.
- Aspires to enable a future of composable, modular devices where extension/customization is the norm. Getting our products to cooperate should not be a constant battle requiring mounds of ugly hacks.
✨Highlights
- Call
SigLink.process_signals()to enable quick-and-easy device control via serial/IO connection.
Details
The framework provides access to device state using something the author calls a "Model-React-Controller" (MRC) -- analogous to a Model-View-Controller.
Code Repository
- Development repository on github: MyState.
- Project examples (self contained): HomeLights_Wired. --- Download:v0.1.
SampleProj: HomeLights_Wired
The HomeLights_Wired project emulates a simplistic home light automation solution. To demonstrate state/signalling capabilities of the MyState libraries, the solution was split across 3 independent microcontrollers:
- A Circuit Playground Bluefruit (#4333) board is used to emulate the lights in your house.
- An Adafruit MacroPad (#5100) acts as a control panel the user can use to toggle on/off lights, as well as dim them to a desired level.
- Finally, a RP2040-pico (#5525) board is used as the "main controller" - taking inputs from the macro pad, and updating the emulated house lights on the Circuit Playground.
- At the moment, the main controller also uses a NeoRotary 4 breakout (#5752) to change RGB values of individual lights.
- More inputs methods could certainly be added if desired. For example, you could use an IR remote's RED/GREEN/BLUE app buttons in conjunction with the volume +/- buttons to change the RGB values from a distance.
- You could even use the channel +/- buttons to switch between which light color is currently being modified.
To make uploading code a bit easier, each set of microcontroller-specific code (excluding common libraries) is stored in its own subfolder (see demos folder):
-
RP2040-pico (#5525) "main controller":
LightCtrl3Boards_2040pico/subfolder. -
Adafruit MacroPad (#5100) (mostly) "dumb terminal":
LightCtrl3Boards_AFMacropad/subfolder. -
Circuit Playground Bluefruit (#4333) - acting as a light controller:
LightCtrl3Boards_CPbluefruit/subfolder.
Block level diagram
Code: Defining system state
The main state of this multi-MCU solution resides on the main RP2040 controller board. Ignoring the less important aspects: this main internal state keeps track of:
- RGB values for the light color,
- the light intensity levels, as well as
- the on/off state of the light.
This information is stored in the StateDef.py file.
STATEBLK_CFG = StateBlock("CFG", [
#Full white for now (overwritten by config_reset.state!):
BGRP_RGB("kitchen", dflt=(255,255,255)),
BGRP_RGB("livingroom", dflt=(255,255,255)),
BGRP_RGB("garage", dflt=(255,255,255)),
#Other rooms lights here...
]
)
STATEBLK_MAIN = StateBlock("Main", [
BFLD_Toggle("kitchen.enabled", dflt=1),
BFLD_Percent_Int("kitchen.level", dflt=100),
BFLD_Toggle("livingroom.enabled", dflt=1),
BFLD_Percent_Int("livingroom.level", dflt=100),
BFLD_Toggle("garage.enabled", dflt=1),
BFLD_Percent_Int("garage.level", dflt=100),
#Other rooms lights here...
)
#Signal entry point for anything wanting to control this device (ex: PC/other uController, ...):
MYSTATE = ListenerRoot([STATEBLK_CFG, STATEBLK_MAIN])
Notice how we are able to split up states in multiple sections:
-
CFG: storing the light RGB values, and -
Main: tracking light intensity, and on/off (enable) state.
To ensure messages get routed to the appropriate section, a "ROOT" object (called MYSTATE in this example) keeps a list of the two state blocks.
Also note that the BGRP_* and BFLD_* functions are preset convenience functions that build state variable groups (like R,G,B triplets), and individual state fields of a certain type. They are used to make object construction a bit more readable.
Code: State-changing signals
The MyState library contains pre-defined signal objects/types that can be used to react to events, modify state with little programming effort. These signals exist as python objects, but also as strings that can be sent through IO channels (ex: UART/RS232/other communications interface/...).
The string representation of signals typically contains 4 parts:
-
TYPE: The signal type identifier. -
section: The section (or state block) being targeted. -
id: The main signal identifier. -
val: The signal value.
As an example, a signal for incrementing kitchen light intensity by a value of 5 might look something like:
- "INC Main:kitchen.level 5".
The following lists the available signals in MyState.Signals:
-
SigEvent: "SIG [SECTION]:[ID] [VALUE]"
- Generic signal that can be sent between devices/microcontrollers/code.
-
SigValue: "SVL [SECTION]:[ID] [VALUE]"- A way to send a value (ex: of a state variable) without necessarily setting anything.
-
SigSet: "SET [SECTION]:[ID] [VALUE]"- Request to change the state variable of a
StateBlockinstance.
- Request to change the state variable of a
-
SigGet: "GET [SECTION]:[ID]"- Request to get the state variable value from a
StateBlockinstance.
- Request to get the state variable value from a
-
SigIncrement: "INC [SECTION]:[ID] [VALUE]"- Request to increment the state variable of a
StateBlockinstance.
- Request to increment the state variable of a
-
SigToggle: "TOG [SECTION]:[ID] [VALUE]"- Request to toggle the state variable of a
StateBlockinstance.
- Request to toggle the state variable of a
-
SigUpdate: "UPD [SECTION]:[ID] [VALUE]"- Request to have a controller send updated values to all of its listeners.
-
SigDump: "DMP [SECTION]"- Request to have a controller dump send all updated values to an IO stream.
- NOTE: "DMP ROOT" tells the
ListenerRootobject to dump state variables of allStateBlocks under its control.
SigDump signal is to provide an automatic discovery system for your internal state variables that doesn't require extra documentation on your part. A step towards interoperability and oneness with the outside world 🧘♀️Ω🧘♂️.
Still work to be done for auto-discovery of other signals, but at least MyState provides some minimal "introspection" on the contents of StateBlock objects.
Code: Signal filtering
Signal filtering takes some input signal in order to convert them to a more practical form. For example, the input signal from a rotary encoder having moved by a certain delta value can be filtered (or converted) to a corresponding change in light intensity. Since the change in intensity might need to go up/down by 5% steps for every click of the rotary encoder, the filtering function would typically be used to scale this input delta value.
For this particular project, the main signal filtering code can be found in LightCtrl3Boards_2040pico\StateReact.py. In this particular case, filtering is performed by a unique instance of the SenseFilter class created in main.py. A highly simplified version of how to use this object is presented here:
#Over simplified example -- for illustration purposes:
#Construction the SenseFilter object:
SENSE_FILT = SenseFilter(STATE_SYNC.roomcache_map)
#...
while True: #The main loop
#If detect SigEvent("MP", "BTNPRESS", btn_idx) from macropad:
SENSE_FILT.filter_keypress(btn_idx)
#If detect SigEvent("MP", "ENCCHANGE", delta) from macropad:
SENSE_FILT.filter_MPencoder(sig.val)
#If detect cange on I2C-attached NeoRotary 4:
SENSE_FILT.filter_I2Cencoder(encoder_idx, delta)
You can look into these filter functions to see how they get translated into state change (SigSet) signals. These signals eventually trigger changes to the STATEBLK_CFG & STATEBLK_MAIN objects described above. Reminder: the "CFG" block stores the RGB "configuration" of the individual lights, whereas the "Main" block stores the on/off state and intensity setting of the lights.
Code: State react
After the state itself gets updated, it automatically notifies any "listener" registered with it. In this example, the listener is an instance of the MainStateSync(StateObserverIF) class located in LightCtrl3Boards_2040pico\StateReact.py. Being derived from the StateObserverIF interface, this class needs to implement a .handle_update() function to react to the state changes:
#Over simplified example -- for illustration purposes:
class MainStateSync(StateObserverIF):
#...
def handle_update(self, section:str):
#NOTE: Triggered after state values get changed
#Compute effective RGB values of all the lights (inefficient, but simple):
for (idx, refc) in self.roomcache_map.items():
#NOTE: roomcache_map stores strings to keep from calculating
# on-the-fly when sending out signals. Not absolutely necessary.
#Update both macropad lights & actual lights on Circuit Playground:
for com in self.update_comlist:
#com is an instance of EasyCktIO.UART.SigCom_UART
#that knows how to send signals:
com.send_signal(self.sig_lightval_update)
Possible enhancements
- Build a special GUI on PC that automatically reads in
MyStatevariables, and creates sliders, toggle switches, etc to alter settings. - Find a way to automatically register any signal being created unless is "private" (not meant to be used externally). Let these signals also be written out on a serial interface when a "DMP ROOT" message is received.
Additional resources
Finding parts needed for a physical project is often a non-trivial part of the design process. If you are like me, you might struggle with this too. Hopefully the suggestions that follow will be useful for your own projects!
Resources: PCBs & mounting
-
Adafruit swirl grids (#5774 +3more): Keep your project together and your wires connected.
- Great for the prototyping stage of your design.
- M2.5 standoff/screws (#3658): Also available in black. You might need other sizes (M2, M3) - but M2.5 appears to be the most common size for hobby PCBs.
-
This mounting plate (#2944): One of the few practical solutions I have found to mount an Adafruit MacroPad to a swirl grid.
- MacroPads can be mounted using M3 screws... but they just don't quite like being tied to the swirl grids. Grids work best with M2.5 screws.
- Thin enough to slip right above the MacroPad bottom plate.
-
1591L proto board: Practically sized breadboard (~2.5in x 1.75in). Reasonably priced.
- Includes 4 x M3 mounting holes: Designed for 1591L enclosure (need model with flanged cover).
- Perfect substrate for sticking under "tiny breadboard" (#65) - which then easily mounts to Adafruit swirl grids.
- Also an overall great form-factor for Today's typical microcontroller project sizes - especially if you need the mounting holes.
-
Canaduino-M proto board: Practically sized breadboard (~3in x 1.74in). Very well priced.
- Includes 4 x M3 mounting holes -- really helps with keeping things together.
- Perfect substrate for sticking under a 50 x 30mm surface box (RJ45 housing) - which then easily mounts to Adafruit swirl grids.
-
Bakelite Perfboard (#2670): Practically sized for sticking under half-sized breadboards (#5422).
- Includes 4 mounting holes - which easily attach to Adafruit swirl grids.
- 90°/L-bracket (#3768): Designed for motors - but great at holding up a Circuit Playground boards perpendicularly atop Adafruit swirl grids.
Resources: Networking
- RS232 Pal (#5987): Simple. Inexpensive. Just what you need.
- 50 x 30mm surface box (RJ45 housing)
-
VCE RJ45 keystone jacks: Good quality. Punch down stand really helps.
- Might consider buying a better punch-down tool that automatically cuts excess wire.
- Sparkfun RJ45 breakout + (separate) jack: Works well on solderless breadboard.
- ...but needs something to keep front of jack from tipping down.
- Suggest trimming stacking headers to 2 pins, bent 90°, with pins themselves clipped to fit in breadboard, and using as a shim between the nubs under the jacks.
- Cat5e cable: This one seems good. With devices/breakout boards as small as they have become, you'll want something relatively thin & flexible to keep your overall builds small as well.
- Some (typ. flat) RJ11 phone cables can be thinner and more flexible than CAT5, but finding one that is might be a challenge.
- As for round CAT3 phone cables: they appear to mostly use solid core conductors - making them less flexible than a good CAT5e alternative.
- In any case: RJ11/12 jacks are surprisingly not that much smaller than RJ45 - might as well just stick with the more ubiquitous RJ45+CAT5e components.
This page (MyState: Be one with the electronics universe 🧘♀️Ω🧘♂️. Collaborate. Interoperate.) was last updated on September 07, 2025.
Text editor powered by tinymce.