This is a translation of Anykey x6, mostly done with the free version of deepl.com and some proofreading.
My sister is a teacher.
A job I don't envy her for - especially in 2020. Here in Bavaria life at school is slowly starting again (as of the beginning of May), but in parallel to the classes in the schools the children who stay at home have to be taught as well. If you take your own work seriously, this is no fun. Sure, you could simply throw worksheets over the fence and refer to pages in the textbooks, but when I think of my own school days and the rather extrinsic motivation...
Although my sister has a lot of digital material, teaching (especially languages) needs someone who speaks in front of the class and of course independent learning isn't easy for everyone. That's why she started making videos. At first only with printed sheets and the mobile phone on the tripod. To be able to use her digital materials better, she asked for a screen recorder. OBS was quickly installed and set up. Too bad she doesn't have a second monitor (or display port adapter) for her MS Surface.
So that her students can see her (or even do "blackboard lessons"), I created several scenes in OBS:
- Screen recording
- Screen recording + camera in the corner
The only stupid thing is that without that second screen she has to bring OBS to the foreground for every action. You could also create global hotkeys, but without a full keyboard you run the risk of getting double assignments and thus have very strange behaviour on the PC. At the same time you don't see what OBS is actually doing.
Yes, you can install a remote control for OBS on your tablet or smartphone, but this only creates more problems than it solves, especially since you have to reconfigure the (free) clients I found right away for each session. Furthermore the device has to stay on and there are no physical buttons to feel. This is unnecessarily distracting and does not really increase acceptance.
Even ready-made solutions like the Elgato Stream Deck are disproportionately expensive for the (hopefully) short period of use.
At the same time, there is an engineer sitting around here who, due to the current situation, is reducing flextime.
How about some tinkering? A few buttons, a few LEDs, a little firmware and a few lines of code on the PC. Why should it be so complex?
Well, let's get one thing straight: If you go beyond your objective it's gonna take a lot of work.
The goal is to have 6-8 colorfully illuminated buttons that can be integrated into OBS.
The microcontroller was easy to select. To be not too far from the code examples of V-USB, a vintage ATmega8 is the way to go.
Since I wanted to do something with intelligent LEDs for a long time, there is a strip with WS2812B lying around way too long. The plan was to glue it - maybe even still on the strip - on push buttons.
Of course, the WAF must fit somehow, so a not too shabby housing is crucial.
Thanks to a 3D printer, I'm able to do this (at least in terms of the equipment).
The disillusionment came faster than expected.
I knew that the timing of the LEDs is critical. That this also applies to V-USB, too. That it is not so easy to combine the two came clear very quickly. The USB stack absolutely needs a pin interrupt, but to control the LEDs, it must be switched off - otherwise everything flashes like a Christmas tree on LSD. Tim (aka cpldcpu) has taken the effort to enable V-USB with Polling - even with a corresponding LED. I didn't want to go that way because it could potentially be painful in other places.
Ok, what now? A coprocessor that converts UART/I²C to the LED protocol? Actually, I didn't want to build a multiprocessor system - and again, the problem with interrupts vs. no interrupts remains, also due to the limitations of USI, this might be a bit uptight.
Good advice is expensive.
But I didn't want to go back to single-color LEDs either.
To control 6 RGB LEDs with PWM, 18 PWM channels are required. I don't think any AVR8 supports that. I have a PCA9685 lying around, providing only 16 channels. In addition the component with its breakout board takes quite a lot of space. Too much for what I have in mind.
Have I written 18 channels? Yes and no. When multiplexing, between 3 and 6 are sufficient. Finally, I decided to take the colors into multiplex and let the PWM run per key - with corresponding losses of brightness (one third). But where do you get 6 synchronous PWM channels on an ATmega8? Very simple: not at all. At least in hardware - it looks different in software, where it's not a real PWM, but rather a pulse power modulation (short PPM - I could find a suitable term, hence the new creation, not to confuse with parts per million or pulse-position modulation, the German abbreviation works a little better), as it was already described in 2011 on the German Schatenseite.
To describe it briefly: with normal PWM there are two switching points for each channel, one of which is individual for each channel (because it determines the brightness value). If you have n channels, there are n points in time which you have to hit as accurately as possible. Especially if they are close together, this can lead to timing problems.
The dimmer used here works differently. You have to keep in mind that when dimming LEDs, it doesn't matter if they are on for a cycle at a time or if they are switched off in between - important is how much they are active during a cycle.
With a resolution of b bit, this method gives you b points in time - no matter how many channels you use and: their timing is fixed. In addition, they are always separated in time by a factor of 2. Now the channel is always switched on when the "bit" of the corresponding time slot is active in the brightness value.
Since the explanation works much better visually than with text, here are two examples with a 3 bit modulation. The valence is on the left, the on times correspond to the green blocks (whose x-axis corresponds to the time) and the necessary interrupts are marked with red arrows:
If you look at the active area of the individual brightness values, you come to the same total area - only that the PPM has a significantly higher efficiency. In addition, there is the effect that by "chopping" the PWM, the LEDs flicker at a higher frequency (even if it varies). This reduces the stroboscopic effect - one person's joy is another's misfortune: considering EMC it becomes more uncomfortable at that point.
But enough about that.
Due to multiplexing, there is, as described above, only a third of the brightness. Most LEDs can handle much more current when driven with pulses than in continuous operation because the limiting factor is heat. Since the LEDs (in the 5050 package) were "harvested" from a LED strip for the assembly, I don't have a datasheet for them, but knowledge: e.g. a 330 Ohm resistor is used for the red strand, with 12 V and 3 red LEDs in series with approx. 1.85 V forward voltage each this is about 20 mA. Without further information about maximum currents and because the brightness should be enough, I stayed with this current.
The buttons are connected directly to IOs and are wired to ground. A rudimentary debouncing is done in software.
The USB circuitry corresponds as far as possible to the examples from Objective Development. Out of sheer laziness I took over the protection circuitry from the USB foot switch. Proverbially: I assembled the necessary components and trimmed the circuit board to the right size.
So the whole circuit is relatively simple. The FETs for the LEDs are BSS84 (p-channel, on the anode side) and BSS138 (n-channel, on the cathode side). Because of the simplicity a hand sketch does the job:
The firmware is surprisingly simple, especially since I didn't try very hard.
The LEDs are controlled with the method described above (in leddrv.c), with an additional fading function. This relieves the USB stack and reduces flickering (see below)
The keys are polled (unless PWM has higher priority) every millisecond (
buttons_tick()). In case the level is low (key pressed), a counter is incremented. If the level is high, the counter is reset to 0.
If the counter is greater than 10, the corresponding key is considered pressed. All buttons in one byte can be queried via the method
As far as USB is concerned, an Out-Report (PC -> Device) with net 16 bytes and an In-Report with net 8 bytes (Device -> PC) is used.
The Out-Report is used to set the LEDs and looks as follows:
> I0 R0 G0 B0 T0 I1 R1 G1 B1 T1 I2 R2 G2 B2 T2 xx
- Ix: Index of the LED (1-6)
- Rx: Red value of the LED (0 ... 255)
- Gx: Green value of the LED (0 ... 255)
- Bx: Blue value of the LED (0 ... 255)
- Tx: Duration for the transition (0 ... 255, in 5 ms steps)
This way, colors for 3 buttons can be written at once, index 0 is not used, because it is usually the init value for arrays and therefore the color of a key is not set by mistake.
The in-report is very simple, the first byte contains the bit pattern of the currently pressed buttons:
< K1 xx xx xx xx xx xx xx
The report is sent every time the status of the buttons changes. This is not quite ideal in several respects, because on the one hand the initial state (after opening the device) cannot be determined and on the other hand, if a USB packet is lost, you miss keystrokes.
But: it had to be implemented quickly.
The mechanics are (at least for me as a noob to design and 3D printing) a bit more complex.
Just banging the circuit board on the table is not possible here, it doesn't have to look spick and span, but it shouldn't completely be "form follows function".
To get ahead quickly, the MS Paint in 3D design was used: SketchUp. With all the limitations and lack of parameterization, it's still the fastest way for me to prepare something for printing.
With a little drawing in 2D software, it turned out that rectangular keys with a width and height of 15 mm, a rounding with a radius of 2.5 mm and a distance of 5 mm to each other have a pleasant look & feel.
A first pattern with a collar (so the key doesn't fall through the case) with transparent PLA, (if I remember correctly) 50 % infill and concentric top/bottom pattern (sliced in Cura) is printed quickly and looks, after being smeared with ink to see how tight the print is, dirty even after cleaning.
Light goes through, but looks rather modest:
Next comes the question: how to assemble the LEDs and switches? to simplify the mechanics, I followed the idea of using common parts where possible. In my last PCB orders I always placed 1x1 mm copper areas in the waste in a 1.27 mm grid, a strip with this is about 9.6 mm wide (with 1.6 mm material thickness) - a good size for this application.
LED on one side, buttons on the other and an idea for which the designers at work would probably (quite rightly) not look at me anymore: Constraining on two reference planes. Normally, you aim to have the tolerances spread out from one side so that you can keep the tolerance chains under control. Would mean here: The keys are referenced to the front of the case and accordingly, the keys have to be mounted on a plane whose tolerance refers to the front.
But in my case the keys should press against the base plate. With many disadvantages. Since it is currently however a single piece: No matter.
The PCB with LED in the front and switch in the back is simply inserted into a groove in the button. Except for a small (reproducible) printing error, this works perfectly. Here are the different design iterations:
anykeyx6_taste_iterationen.jpg | iterations of keys 1 to 5.5 anykeyx6_sketchup_taste_seite.png | Final model of the keys from the side anykeyx6_sketchup_taster_unten.png | and from below anykeyx6_taste_einzelteile.jpg | individual parts of the keys (w/o switch) anykeyx6_taste_LED.jpg | circuit board inserted into the button
With infill set to 100 % and the button front on the printing bed, a relatively nice surface is created. Since Concentric Top/Bottom-Pattern created a cross in the diagonal, I switched to "Lines", which, together with the concentric frame, makes the push buttons look almost "like a bought one".
In order to achieve better color mixing (and the buttons have a strange scattering effect), I glued some tape from my label printer (if it is important: Brother P-touch) behind the button. This makes the illumination of the three LED crystals at least a bit more homogeneous:
In total the sandwich has a height of 12.4 mm - 0.25 mm less when the button is pressed. The collar is offset 2.5 mm to the inside. This, plus the thickness of the case parts, becomes the total thickness of the device.
The dimensions of the buttons are the starting point, with the cut-outs for them extended by 0.25 mm in all directions. This brings some play, but before the cutter has to be attached or printed again, the keys may wobble a little - especially since there were some elephant feet in the first keys. With a projection of 7.5 mm from the edge of the buttons to the case walls in three directions and a little more (17.5 mm) in the fourth direction - the electronics have to go somewhere - the case is relatively compact. Inside, there are 4 mm high walls between the buttons - not for alignment, but rather to avoid stray light between the buttons. To prevent the housing from bending and thus accidentally actuating several buttons at once, there are elevations in the crossing points of the buttons which increase stability in the Z-direction.
To save material and especially printing time, there were two cut-off test prints, which are almost invisible on a white background on white background thanks to the transparent filament:
As strain relief for the permanently installed USB cable, there is a small ramp behind the U-shaped cut-out and a hole through for a cable tie. The ramp only serves to prevent the cable tie from pressing down the lower half of the housing. One could speculate that the stair structure caused by the print layers might even help the cable to not slip out.
3 holes, through which M2.5 screws fit, are used to hold everything together.
There were 4 iterations in total, but only two of them found their way to the printer - and that only because the first part lifted off the printing bed and thus was warped.
Apparently the STL data is not quite clean, so the model is probably a bit hanging in the air. With a microstep of -0.001 in the Z-direction, the printing result was better, but the first layer is still a bit grubby...
The lower part of the case is actually 7 parts and needed no revision (but a small operation with the scalpel). The complexity is also much lower. The 4 "chimneys" are striking at first - the idea behind them is to insert nuts and screw them to the upper part of the case. The whole thing is (rather unintentionally) designed for compression, both for the nut and towards the other part of the housing. At least nothing slips.
There are blind holes/indents at the positions of the buttons - for a very simple reason: save material and print time. The base plate should be thick enough to be stable but not to have to be printed a second time.
In order to fit the buttons in height, small spacer nipples are used, which are small and therefore can be adjusted and printed very quickly. Because I didn't have any super glue left, they are only fixed on one side on the base plate with double sided adhesive tape.
All in all, the printer was busy for about 4 hours, although it was not running at maximum speed. The material costs are (without samples and misprints) about 50 cent if I am not mistaken, so not really worth mentioning.
Originally I planned to use an Atmega8 in a DIP package. But since it would have been a bit tight, I decided to use one in a TQFP package. Besides the microcontroller, the 9 transistors for LED control are also located on the self-made dot matrix board:
The LED keys are fitted with long connecting leads made of enamelled copper wire, which are beeped through and stuck on correspondingly labelled tape strips. Twisted and soldered together, they then are connected the FETs. Same procedure for the ground of the switches.
To prevent the keys from repeatedly slipping out of their holes when the housing is closed, tape is applied to the outside of the case. For safety reasons, it is labeled with numbers so that nothing can go wrong with turning and mirroring. Keys inserted, cables laid in the case and the copper ball is formed. Cable spaghetti, anyone?
The USB cable is firmly lashed down and has a very tight bending radius in the case - it's in (hopefully).
The nipples are already glued into the lower half of the housing and it gets exciting: does it fit?
Unfortunately there are no countersunk screws in the part box, so it remains a bit ugly with the galvanized cylinder head screws. Or pragmatic. Well, it holds - and the buttons are in the case in such a way that they neither (too much) rattle nor are they pressed permanently. Success! :)
To give the buttons some more meaning, the upper row is decorated with a label of some symbols from FontAwesome. Now nothing should go wrong.
The first test software is written in Python and lets a rainbow run through. For the sake of simplicity, it only changes one LED at a time:
import hid import time import colorsys devices = hid.enumerate(0x16C0, 0x05DF) if len(devices) == 0: print("no devices found") exit() dev = hid.device() dev.open_path(devices['path']) def setled(dev, index, color): r = int(color * 255) g = int(color * 255) b = int(color * 255) dev.write([0, index + 1, r, g, b, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) d = 60 while True: for h in range(0, 360): for i in range(0, 6): setled(dev, i, colorsys.hsv_to_rgb(((h + d * i) % 360) / 360, 1, 1)) time.sleep(0.02)
As a snapshot, it looks something like this:
The OBS Client
Due to the lack of deep knowledge in C++, I don't even try to write a real plugin for OBS. Instead I use obs-websocket, which provides an API for C# that is extremely easy to use.
For the buttons, an extremely rudimentary class is hacked together. No error handling at all. The buttons are read out in a background worker that fires events (and also determines the duration of the press when released), the LED colors are set for all 6 buttons in one method.
Here you can also see how hastily cobbled together it is: Between writing the two "sets" of color, the software sleeps for a millisecond. The reason is both simply and stupid: if you write too fast, the first of the two data packets may be lost in the firmware, because the transfer is not (if you want to call it that) threadsafe.
The GUI only has a "Connect" button that should only be pressed when OBS is running and the hardware is connected. The configuration is also rather basic and can be found in AnykeyOBS.exe.config
Here you can store the active/inactive colors for each button as hex code (or named color, untested), define the function and give an argument for this function.
To assign a scene, pack into the function "Scene" and write the corresponding scene name into the argument.
For recording, the function is called "Record" and the argument can be defined as "Toggle", "Start" and "Stop". Streaming is not yet supported.
For reasons not quite understandable to me, the state of streaming/recording can only be determined by events. Therefore, the key with the corresponding assignment remains orange when the program is started. This is also the (hard coded) color for transitions between recording and no recording.
Another feature is the cancelling of actions - basically they are only triggered when the button is released - unless the button is held for more than one second. In this case the action is cancelled.
All in all, the software can at best be described as a proof of concept. It works, somehow.
In the meantime I was even allowed to visit my sister to give her the hardware. yay! But the setup was done online, though. Got obs-websocks installed, copied AnykeyOBS on the tablet and: Exception.
************** Ausnahmetext ************** System.ComponentModel.Win32Exception (0x80004005): Element nicht gefunden bei SimplerHid.HidDevice.GetName(IntPtr handle) bei SimplerHid.HidDevice.GetInfoSets() bei AnykeyOBS.Anykey6.Connect(Int16 vid, Int16 pid, String SerialNumber) bei AnykeyOBS.Form1.btnConnect_Click(Object sender, EventArgs e) bei System.Windows.Forms.Control.OnClick(EventArgs e) bei System.Windows.Forms.Button.OnClick(EventArgs e) bei System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent) bei System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks) bei System.Windows.Forms.Control.WndProc(Message& m) bei System.Windows.Forms.ButtonBase.WndProc(Message& m) bei System.Windows.Forms.Button.WndProc(Message& m) bei System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m) bei System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m) bei System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)
The presumed cause is quickly located: Some USB devices' name can't be read. Thrown a pack of try-catch on the problem and a pinch of
Console.WriteLines and the program stays open after clicking "Connect".
"Do the buttons light up?" - "Oh man, how cool" - "So yes?"
After that I only heard wild clicking. I think she likes it.
And this is what it looks like:
Where to start? For the good, the bad or the things I'm ashamed of?
Bottom line: for a very shirt-sleeved piece of hardware, it looks (without me wanting to praise myself) quite decent. It only gets bad when you look under the hood. But at least that's pretty consistent.
- My sister has something that I hope will make her job a little easier
- I've learned a about 3D printing again
- It's been a lot of fun
- It's quite colorful
- Decoupling, what decoupling?
- Not very bright
- Flickering during USB traffic (due to INT0)
- Too bright in dark surroundings and poorly visible in bright daylight
- Changes of dropped packets for LED settings and key presses are quite high
- Can also be operated from the bottom of the housing
- Relatively loud (can be heard in the video)
- Wobble, are crooked and have no guidance
- Rudimentary in all manners, if anything fails, everything's gone
- No automatic connection to OBS and the hardware
- No GUI for configuration
- Virtually no functions supported in OBS
- No other functions except OBS control
- No standby states
- Datei:Anykeyx6.zip SketchUp files, associated STLs, firmware and C# software to talk to OBS.