Micro PinOS is a modular Micro PinOS — Nano switch nodes on the playfield, Mega 2560 game brain in the cabinet, connected over a shared RS485 bus.
Each Nano debounces its switches and fires local solenoids — pop bumpers and slings respond in microseconds, no Mega roundtrip.
Switch events travel as compact serial frames over one shared twisted pair. Up to five Nanos share the bus back to the Mega.
The Mega dispatches events to the active GameStyle plugin, manages scoring, ball tracking, and all high-power outputs.
SOUND, SCORE, and LIGHT commands go to dedicated secondary boards over separate serial channels.
The Mega's loop() does exactly four things, in order, every pass. No blocking delays anywhere. The Nano sketch is a single config table at the top — change the pins and IDs, reflash, done.
All game logic lives in GameStyle .h files. The OS knows nothing about scoring rules.
// MainMega.ino — main loop, four calls only
void loop() {
readRS485(); // drain bus → circular queue
processNextEvent(); // one event → dispatcher
updateGameLogic(); // state machine + timers
updateSolenoids(); // non-blocking coil pulses
}
// RS485 frame parser — "S<id>:<state>\n"
void readRS485() {
while (Serial1.available()) {
char c = (char)Serial1.read();
if (c == '\n') {
rs485Buf[rs485Pos] = '\0';
if (rs485Buf[0] == 'S') {
char *col = strchr(rs485Buf+1, ':');
if (col) enqueue(atoi(rs485Buf+1),
atoi(col+1));
}
rs485Pos = 0;
} else if (rs485Pos < sizeof(rs485Buf)-1) {
rs485Buf[rs485Pos++] = c;
}
}
}
// nano_universal.ino — configure at top, done
#define MY_ADDRESS 0 // unique per Nano (0–4)
#define DE_PIN 2 // MAX485 transmit enable
// pin | switchID | solPin | holdPin | pulseMs | debounceMs
// Type 1 Sensor: solPin = -1, holdPin = -1
// Type 2 Reaction: solPin >= 0, holdPin = -1
// Type 3 Flipper: solPin = power, holdPin = hold
static const SwEntry SW[] = {
// Nano 0 — Slingshots + pop bumpers (Type 2)
{ 3, SW_SLINGSHOT_LEFT, 5, -1, 25, 5 },
{ 4, SW_SLINGSHOT_RIGHT, 6, -1, 25, 5 },
{ 7, SW_POP_BUMPER_TOP, 8, -1, 20, 5 },
{ 9, SW_POP_BUMPER_LEFT, 10, -1, 20, 5 },
{ 11, SW_POP_BUMPER_RIGHT,12, -1, 20, 5 },
};
// Nothing below this line needs editing
void loop() {
readSerial(); // mode commands from Mega
handleSwitches(); // debounce, fire, report
handleSolenoids(); // kick/hold timing
}
// Event dispatcher — no game logic lives here
void dispatchEvent(int sw, int state) {
// P1: Slam tilt — kills everything, any state
if (sw == SW_SLAM_TILT && state == 1) {
gameState = STATE_GAME_OVER;
broadcastNanoMode(MODE_ATTRACT);
sendSound(SND_SLAM_TILT);
sendVideo(VID_SLAM_TILT);
return;
}
// P2: Attract — wheel + coin handling
if (gameState == STATE_ATTRACT) {
handleAttractEvent(sw, state);
return;
}
if (gameState != STATE_GAME_RUNNING) return;
// P3: Tilt — disable flippers, broadcast
if (sw == SW_TILT && state == 1) {
if (!tiltActive) {
tiltActive = true;
sendSound(SND_TILT);
sendLight(LGT_FLIPPERS_OFF);
broadcastNanoMode(MODE_TILT);
}
return;
}
// P4: Drain → P5: Trough → P6: Game shot
if (sw == SW_DRAIN && state == 1) { handleDrain(); return; }
if (currentGame) currentGame->onShot(sw);
}
// Non-blocking solenoid manager — millis() only
struct Solenoid {
uint8_t pin;
bool active;
unsigned long endMs;
};
Solenoid solenoids[NUM_SOLENOIDS] = {
{ PIN_TROUGH, false, 0 }, // [0]
{ PIN_KNOCKER, false, 0 }, // [1]
{ PIN_SLING_LEFT, false, 0 }, // [2]
{ PIN_SLING_RIGHT, false, 0 }, // [3]
{ PIN_POP_LEFT, false, 0 }, // [4]
// ...
};
void fireSolenoid(int idx, unsigned long ms = SOL_PULSE_MS) {
digitalWrite(solenoids[idx].pin, HIGH);
solenoids[idx].active = true;
solenoids[idx].endMs = millis() + ms;
}
void updateSolenoids() {
unsigned long now = millis();
for (int i = 0; i < NUM_SOLENOIDS; i++) {
if (solenoids[i].active && now >= solenoids[i].endMs) {
digitalWrite(solenoids[i].pin, LOW);
solenoids[i].active = false;
}
}
}
One nano_universal.ino handles all three node types. Configure the switch table at the top and flash — no library forks, no edits below the table.
Up to five Nanos share one twisted pair at 115200 baud. Noise-immune across the full playfield — no ground loops, no long vulnerable UART runs.
Define multiple game modes in separate .h files with a five-function interface. The attract wheel lets players choose before ball launch.
All coil timing uses millis() — never delay(). The Solenoid struct tracks active pulses so the loop never stalls even at peak activity.
A shared commands.h defines every SOUND, LIGHT, VIDEO, and SCORE string. No magic numbers anywhere — one file to rule all boards.
High scores survive power cycles. Attract mode shows rankings; initials-entry lives in the OS layer, completely separate from each GameStyle.
Each game mode is a .h file that implements five functions. The OS handles everything else — state machine, ball tracking, solenoids, EEPROM. Your game style only thinks about scoring.
// Five functions — that's the entire interface
struct GameStyle {
const char *name;
void (*onInit)(); // game start
void (*onBallReset)(); // each new ball
void (*onShot)(int sw); // playfield events
void (*onTimerTick)(); // mode timers
void (*onBallDrain)(); // bonus + cleanup
};
// Register all four styles
const GameStyle gameStyles[NUM_STYLES] = {
{ "Game Style One",
g1Init, g1BallReset,
g1Shot, g1TimerTick, g1BallDrain },
{ "Game Style Two",
g2Init, g2BallReset,
g2Shot, g2TimerTick, g2BallDrain },
// ... Three, Four
};
// Scoring constants — top of file
#define G1_PTS_BUMPER_NORM 100L
#define G1_PTS_BUMPER_FRENZY 500L
#define G1_PTS_SLING 50L
#define G1_PTS_LANE 500L
#define G1_PTS_TARGET 300L
#define G1_PTS_RAMP 1000L
#define G1_BUMPERS_FOR_FRENZY 75
#define G1_FRENZY_MS 30000UL
void g1Shot(int sw) {
if (tiltActive) return; // tilt gate
switch (sw) {
case SW_LANE_F:
g1LaneHit[0] = true;
addScore(G1_PTS_LANE);
sendSound(SND_ROLLOVER);
sendLight(LGT_LANE_F);
g1CheckLanes(); // advance mult if all lit
break;
case SW_POP_LEFT:
case SW_POP_RIGHT:
case SW_POP_CENTER:
g1OnBumper(sw); // frenzy counter inside
break;
case SW_RAMP_LEFT:
g1OnRamp(true); // extra ball counter inside
break;
// ...
}
}
attract.h owns the wheel selector — flippers cycle the game list, start button launches. commands.h is the single source of truth for every string that crosses a serial port.
Secondary boards only need to parse the TYPE field and look up PAYLOAD here. No magic numbers, no hardcoded strings scattered across files.
View Switch Reference →// attract.h — wheel selector
#define WHEEL_AUTO_MS 4000UL // auto-cycle idle timeout
void handleAttractEvent(int sw, int state) {
if (state != 1) return;
// Coin in — any slot
if (sw == SW_COIN_LEFT ||
sw == SW_COIN_CENTER ||
sw == SW_COIN_RIGHT) {
if (credits < MAX_CREDITS) {
credits++;
sendSound(SND_COIN_IN);
sendLight(LGT_CREDITS);
}
return;
}
// Flippers cycle the wheel
if (sw == SW_FLIPPER_LEFT) {
wheelPos = (wheelPos - 1 + NUM_STYLES) % NUM_STYLES;
sendSound(SND_ROLLOVER);
sendWheelUpdate(); // WHEEL:N → display board
return;
}
if (sw == SW_FLIPPER_RIGHT) {
wheelPos = (wheelPos + 1) % NUM_STYLES;
sendSound(SND_ROLLOVER);
sendWheelUpdate();
return;
}
// Start button — launch selected style
if (sw == SW_START) {
if (credits > 0) startGame(wheelPos);
else sendSound(SND_NO_CREDITS);
}
}
// commands.h — SOUND → Serial2
// TYPE = "SOUND", secondary board maps to DFPlayer track
// System / attract
#define SND_ATTRACT "ATTRACT"
#define SND_COIN_IN "COIN_IN"
#define SND_GAME_START "GAME_START"
#define SND_BALL_READY "BALL_READY"
#define SND_NEXT_BALL "NEXT_BALL"
#define SND_GAME_OVER "GAME_OVER"
// Playfield hits
#define SND_BUMPER "BUMPER"
#define SND_SLING "SLING"
#define SND_ROLLOVER "ROLLOVER"
#define SND_ROLLOVER_COMPLETE "ROLLOVER_COMPLETE"
#define SND_TARGET "TARGET"
#define SND_TARGET_BANK_COMPLETE "TARGET_BANK_COMPLETE"
#define SND_RAMP "RAMP"
#define SND_JACKPOT "JACKPOT"
// Modes
#define SND_FRENZY_START "FRENZY_START"
#define SND_FRENZY_END "FRENZY_END"
#define SND_BUMPER_FRENZY "BUMPER_FRENZY"
#define SND_EXTRA_BALL_LIT "EXTRA_BALL_LIT"
#define SND_EXTRA_BALL "EXTRA_BALL"
// Danger
#define SND_TILT "TILT"
#define SND_SLAM_TILT "SLAM_TILT"
#define SND_DRAIN "DRAIN"
// Usage:
void sendSound(const char *n) {
sendCommand("SOUND", n, Serial2);
}
// commands.h — LIGHT → Serial3
// TYPE = "LIGHT", maps to lamp index / LED address
// Game state
#define LGT_ATTRACT "ATTRACT"
#define LGT_GAME_START "GAME_START"
#define LGT_BALL_RESET "BALL_RESET"
#define LGT_TILT "TILT"
#define LGT_FLIPPERS_ON "FLIPPERS_ON"
#define LGT_FLIPPERS_OFF "FLIPPERS_OFF"
// FIRE rollover lanes
#define LGT_LANE_F "LANE_F"
#define LGT_LANE_I "LANE_I"
#define LGT_LANE_R "LANE_R"
#define LGT_LANE_E "LANE_E"
#define LGT_LANES_RESET "LANES_RESET"
#define LGT_LANES_COMPLETE "LANES_COMPLETE"
// Bonus multiplier
#define LGT_MULT_ADVANCE "MULT_ADVANCE"
#define LGT_MULT_2X "MULT_2X"
#define LGT_MULT_3X "MULT_3X"
#define LGT_MULT_4X "MULT_4X"
#define LGT_MULT_5X "MULT_5X"
// Modes
#define LGT_FRENZY_ON "FRENZY_ON"
#define LGT_FRENZY_OFF "FRENZY_OFF"
#define LGT_EXTRA_BALL "EXTRA_BALL"
#define LGT_PLUNGE "PLUNGE"
// Solenoid commands → Serial3
#define SOL_CMD_RESET_TARGETS "RESET_TARGETS"
#define SOL_CMD_KICKBACK "KICKBACK"
#define SOL_CMD_EJECT_1 "EJECT_1"
The Quick Start guide walks you from blank Mega to a working ball-in-play state, step by step.