An astronomical observatory dome controller based on two Arduino boards. This is the PlatformIO reorganization of the original ArduinoDomeController project, with build variants for the different observatories where it is installed.
The project builds two firmwares plus a small test utility:
src/master/— the main controller. Drives the dome azimuth motor, reads the rotation encoder and the home sensor, and talks to the PC over USB using the MaxDome II serial protocol (19200 baud) for compatibility with existing astronomy software. When the installation has a motorized shutter, it also acts as the radio bridge to the shutter controller.src/slave/— the shutter controller. Mounted on the rotating dome and powered by a 12 V battery, it drives the shutter (and optionally a flap) DC motors, reads the limit switches, monitors the battery voltage and handles a local keypad.src/keypad-test/— a small utility that prints keypad events to the serial port, used to calibrate analog keypad thresholds.
Shared code lives in lib/:
lib/MotorDriver/— DC motor abstraction (MotorDriver), with a generic two-pin driver (DCMotor) and a Monster Moto Shield driver (MMSMotor).lib/Keypad/— analog (resistor ladder on one analog pin) and digital keypads with debounce, hold and double-click events.lib/RadioLink/— the framed protocol codec used on the master↔slave radio link (see below).lib/AzGeometry/— pure azimuth geometry helpers (shortest path, distances, tick counter to position conversion), free of hardware dependencies so they can be unit-tested on the host.
- The main controller connects to the PC over USB.
- Azimuth is measured with an optical encoder coupled to a friction wheel; a home sensor provides the absolute reference.
- The azimuth motor is driven either by a generic two-signal DC driver/VFD interface or by a Monster Moto Shield, depending on the build variant.
- The shutter controller communicates with the master through HC-12 serial radio modules at 9600 baud.
- Shutter and flap motors use limit switches for the closed/open positions, plus an interference switch on two-shutter installations (the flap cannot open while the shutter is closed, and the shutter cannot close while the flap is open).
- Pin assignments are defined at the top of
src/master/main.cppandsrc/slave/main.cpp.
Brushed DC motors (especially when driven with PWM, as with the Monster Moto Shield) inject noise into the encoder lines that can show up as spurious counts. The firmware decodes the encoder with a quadrature state machine that rejects illegal transitions and cancels out glitches on a single channel, so noise cannot accumulate as position error — but keeping the noise out of the inputs in the first place is still recommended:
- Add external pull-up resistors (1–2.2 kΩ to 5 V) on both encoder channels. The AVR internal pull-ups (~35 kΩ) leave the lines in high impedance, making them very sensitive to motor noise. This is the single most effective measure.
- Add an RC low-pass filter on each channel, close to the Arduino: 1 kΩ in series plus 10–100 nF to GND. Dome encoders produce pulses in the Hz range, so a cutoff of a few hundred Hz filters aggressively without losing real pulses. Optionally add a Schmitt trigger buffer (74HC14) after the filter to restore clean edges.
- Use shielded twisted-pair cable for the encoder, with the shield connected to GND only at the Arduino end, and route it away from the motor wires.
- Suppress the noise at the motor: a 100 nF ceramic capacitor across the motor terminals and, if the case is metallic, 47–100 nF from each terminal to the case. Ferrite beads on the motor leads also help.
- Keep motor and logic grounds separate, joined at a single star point; never share a ground return between the motor and the encoder.
- For long encoder cables (several meters), use a differential line driver/receiver pair (RS-422, e.g. AM26LS31/AM26LS32).
Master and slave exchange ASCII frames over the HC-12 link:
$<payload>*<XX>\n
where XX is the XOR of all payload bytes, in two uppercase hex digits.
Corrupt, overlong or malformed frames are silently discarded and the parser
resynchronizes on the next $.
| Master → slave | Slave → master | Meaning |
|---|---|---|
open, open1 |
ack,<cmd> |
Open both shutters / main shutter only |
close, close1 |
ack,<cmd> |
Close both shutters / main shutter only |
abort |
ack,abort |
Stop shutter movement |
exit |
ack,exit |
Close shutters and disable the watchdog |
stat |
stat,<n> |
Query the combined shutter state (0-5) |
vbat |
vbat,<nnnn> |
Query the battery ADC reading |
The master polls stat every second and vbat every 10 seconds from a
non-blocking state machine (300 ms response timeout, 3 attempts, automatic
recovery), caching the results so the MaxDome II protocol is answered
instantly. Action commands take priority over polls and are acknowledged.
Any valid frame refreshes the slave's watchdog: if the link stays silent
for 60 s (COMMAND_TIMEOUT) the shutters close automatically, protecting
the telescope if the radio link or the master fails.
Each environment in platformio.ini corresponds to a specific observatory
installation with its own hardware configuration:
| Environment | Board | Configuration |
|---|---|---|
master-vst, slave-vst |
Arduino Uno ×2 | Two interfering shutters (Monster Moto Shield), analog keypads, HC-12 link |
slave-ictea |
Arduino Nano (ATmega168) | Single shutter with a generic DC driver, digital keypad |
master-deva |
Arduino Uno | Azimuth only, generic DC driver |
master-carda |
Arduino Diecimila (ATmega328) | Azimuth only, Monster Moto Shield, 4:1 encoder divider |
keypad-test |
Arduino Nano (ATmega168) | Keypad calibration utility |
Build and flash a variant with:
pio run -e <environment> -t uploadPer-installation options are set through build_flags (NSHUTTERS,
ANALOG_KEYPAD, MONSTER_SHIELD, ENCODER_DIV, AZ_TIMEOUT...); see
platformio.ini and the defaults in src/master/dome.h.
Note: master and slave firmwares must be flashed from the same revision — the radio protocol is not compatible with the pre-RadioLink plain-text versions.
The hardware-independent code (lib/RadioLink, lib/AzGeometry) has unit
tests that run natively on the host, without any board attached:
pio test -e nativeMIT — see the headers in the source files.