diff --git a/Activity/Observer.hpp b/Activity/Observer.hpp
new file mode 100644
index 000000000..2690dc0a0
--- /dev/null
+++ b/Activity/Observer.hpp
@@ -0,0 +1,51 @@
+//
+//  ActivityObserver.h
+//  Clock Signal
+//
+//  Created by Thomas Harte on 07/05/2018.
+//  Copyright © 2018 Thomas Harte. All rights reserved.
+//
+
+#ifndef ActivityObserver_h
+#define ActivityObserver_h
+
+#include <string>
+
+namespace Activity {
+
+/*!
+	Provides a purely virtual base class for anybody that wants to receive notifications of
+	'activity' — any feedback from an emulated system which a user could perceive other than
+	through the machine's native audio and video outputs.
+
+	So: status LEDs, drive activity, etc. A receiver may choose to make appropriate noises
+	and/or to show or unshow status indicators.
+*/
+class Observer {
+	public:
+		/// Announces to the receiver that there is an LED of name @c name.
+		virtual void register_led(const std::string &name) = 0;
+
+		/// Announces to the receiver that there is a drive of name @c name.
+		virtual void register_drive(const std::string &name) = 0;
+
+		/// Informs the receiver of the new state of the LED with name @c name.
+		virtual void set_led_status(const std::string &name, bool lit) = 0;
+
+		enum class DriveEvent {
+			StepNormal,
+			StepBelowZero,
+			StepBeyondMaximum
+		};
+
+		/// Informs the receiver that the named event just occurred for the drive with name @c name.
+		virtual void announce_drive_event(const std::string &name, DriveEvent event) = 0;
+
+		/// Informs the receiver of the motor-on status of the drive with name @c name.
+		virtual void set_drive_motor_status(const std::string &name, bool is_on) = 0;
+
+};
+
+}
+
+#endif /* ActivityObserver_h */
diff --git a/Activity/Source.hpp b/Activity/Source.hpp
new file mode 100644
index 000000000..9db6b7a53
--- /dev/null
+++ b/Activity/Source.hpp
@@ -0,0 +1,24 @@
+//
+//  ActivitySource.h
+//  Clock Signal
+//
+//  Created by Thomas Harte on 07/05/2018.
+//  Copyright © 2018 Thomas Harte. All rights reserved.
+//
+
+#ifndef ActivitySource_h
+#define ActivitySource_h
+
+#include "Observer.hpp"
+
+namespace Activity {
+
+class Source {
+	public:
+		virtual void set_activity_observer(Observer *observer) = 0;
+};
+
+}
+
+
+#endif /* ActivitySource_h */
diff --git a/Analyser/Dynamic/MultiMachine/MultiMachine.cpp b/Analyser/Dynamic/MultiMachine/MultiMachine.cpp
index 9e33047dd..5f7a92f29 100644
--- a/Analyser/Dynamic/MultiMachine/MultiMachine.cpp
+++ b/Analyser/Dynamic/MultiMachine/MultiMachine.cpp
@@ -22,6 +22,10 @@ MultiMachine::MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machin
 	crt_machine_.set_delegate(this);
 }
 
+Activity::Source *MultiMachine::activity_source() {
+	return nullptr; // TODO
+}
+
 ConfigurationTarget::Machine *MultiMachine::configuration_target() {
 	if(has_picked_) {
 		return machines_.front()->configuration_target();
diff --git a/Analyser/Dynamic/MultiMachine/MultiMachine.hpp b/Analyser/Dynamic/MultiMachine/MultiMachine.hpp
index de343e9c3..f1a8ea845 100644
--- a/Analyser/Dynamic/MultiMachine/MultiMachine.hpp
+++ b/Analyser/Dynamic/MultiMachine/MultiMachine.hpp
@@ -50,6 +50,7 @@ class MultiMachine: public ::Machine::DynamicMachine, public MultiCRTMachine::De
 		static bool would_collapse(const std::vector<std::unique_ptr<DynamicMachine>> &machines);
 		MultiMachine(std::vector<std::unique_ptr<DynamicMachine>> &&machines);
 
+		Activity::Source *activity_source() override;
 		ConfigurationTarget::Machine *configuration_target() override;
 		CRTMachine::Machine *crt_machine() override;
 		JoystickMachine::Machine *joystick_machine() override;
diff --git a/Components/6522/6522.hpp b/Components/6522/6522.hpp
index 45ef59fc9..d21e2190c 100644
--- a/Components/6522/6522.hpp
+++ b/Components/6522/6522.hpp
@@ -113,6 +113,9 @@ template <class T> class MOS6522: public MOS6522Base {
 		/*! Gets a register value. */
 		uint8_t get_register(int address);
 
+		/*! @returns the bus handler. */
+		T &bus_handler();
+
 	private:
 		T &bus_handler_;
 
diff --git a/Components/6522/Implementation/6522Implementation.hpp b/Components/6522/Implementation/6522Implementation.hpp
index eac2a0648..5efe8905e 100644
--- a/Components/6522/Implementation/6522Implementation.hpp
+++ b/Components/6522/Implementation/6522Implementation.hpp
@@ -145,6 +145,10 @@ template <typename T> uint8_t MOS6522<T>::get_port_input(Port port, uint8_t outp
 	return (input & ~output_mask) | (output & output_mask);
 }
 
+template <typename T> T &MOS6522<T>::bus_handler() {
+	return bus_handler_;
+}
+
 // Delegate and communications
 template <typename T> void MOS6522<T>::reevaluate_interrupts() {
 	bool new_interrupt_status = get_interrupt_line();
diff --git a/Components/DiskII/DiskII.cpp b/Components/DiskII/DiskII.cpp
index da897c36c..373c5ccf6 100644
--- a/Components/DiskII/DiskII.cpp
+++ b/Components/DiskII/DiskII.cpp
@@ -236,3 +236,7 @@ uint8_t DiskII::trigger_address(int address, uint8_t value) {
 	return 0xff;
 }
 
+void DiskII::set_activity_observer(Activity::Observer *observer) {
+	drives_[0].set_activity_observer(observer, "Drive 1", true);
+	drives_[1].set_activity_observer(observer, "Drive 2", true);
+}
diff --git a/Components/DiskII/DiskII.hpp b/Components/DiskII/DiskII.hpp
index fd37e9353..37e083ef8 100644
--- a/Components/DiskII/DiskII.hpp
+++ b/Components/DiskII/DiskII.hpp
@@ -15,6 +15,8 @@
 #include "../../Storage/Disk/Disk.hpp"
 #include "../../Storage/Disk/Drive.hpp"
 
+#include "../../Activity/Observer.hpp"
+
 #include <array>
 #include <cstdint>
 #include <vector>
@@ -40,6 +42,8 @@ class DiskII:
 		void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
 		bool is_sleeping() override;
 
+		void set_activity_observer(Activity::Observer *observer);
+
 	private:
 		enum class Control {
 			P0, P1, P2, P3,
diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp
index db1720cb5..fa60a0d4f 100644
--- a/Machines/AmstradCPC/AmstradCPC.cpp
+++ b/Machines/AmstradCPC/AmstradCPC.cpp
@@ -20,6 +20,7 @@
 #include "../Utility/MemoryFuzzer.hpp"
 #include "../Utility/Typer.hpp"
 
+#include "../../Activity/Source.hpp"
 #include "../ConfigurationTarget.hpp"
 #include "../CRTMachine.hpp"
 #include "../KeyboardMachine.hpp"
@@ -607,6 +608,10 @@ class FDC: public Intel::i8272::i8272 {
 		void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
 			drive_->set_disk(disk);
 		}
+
+		void set_activity_observer(Activity::Observer *observer) {
+			drive_->set_activity_observer(observer, "Drive 1", true);
+		}
 };
 
 /*!
@@ -690,7 +695,8 @@ class ConcreteMachine:
 	public Utility::TypeRecipient,
 	public CPU::Z80::BusHandler,
 	public Sleeper::SleepObserver,
-	public Machine {
+	public Machine,
+	public Activity::Source {
 	public:
 		ConcreteMachine() :
 			z80_(*this),
@@ -995,6 +1001,12 @@ class ConcreteMachine:
 			return &keyboard_mapper_;
 		}
 
+		// MARK: - Activity Source
+		void set_activity_observer(Activity::Observer *observer) override {
+			if(has_fdc_) fdc_.set_activity_observer(observer);
+		}
+
+
 	private:
 		inline void write_to_gate_array(uint8_t value) {
 			switch(value >> 6) {
diff --git a/Machines/AppleII/AppleII.cpp b/Machines/AppleII/AppleII.cpp
index f75655762..e312e05b8 100644
--- a/Machines/AppleII/AppleII.cpp
+++ b/Machines/AppleII/AppleII.cpp
@@ -8,6 +8,7 @@
 
 #include "AppleII.hpp"
 
+#include "../../Activity/Source.hpp"
 #include "../ConfigurationTarget.hpp"
 #include "../CRTMachine.hpp"
 #include "../KeyboardMachine.hpp"
@@ -24,6 +25,7 @@
 
 #include "../../Analyser/Static/AppleII/Target.hpp"
 
+#include <array>
 #include <memory>
 
 namespace {
@@ -34,7 +36,8 @@ class ConcreteMachine:
 	public KeyboardMachine::Machine,
 	public CPU::MOS6502::BusHandler,
 	public Inputs::Keyboard,
-	public AppleII::Machine {
+	public AppleII::Machine,
+	public Activity::Source {
 	private:
 		struct VideoBusHandler : public AppleII::Video::BusHandler {
 			public:
@@ -62,8 +65,8 @@ class ConcreteMachine:
 			speaker_.run_for(audio_queue_, cycles_since_audio_update_.divide(Cycles(audio_divider)));
 		}
 		void update_cards() {
-			for(int c = 0; c < 7; ++c) {
-				if(cards_[c]) cards_[c]->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_);
+			for(const auto &card : cards_) {
+				if(card) card->run_for(cycles_since_card_update_, stretched_cycles_since_card_update_);
 			}
 			cycles_since_card_update_ = 0;
 			stretched_cycles_since_card_update_ = 0;
@@ -80,7 +83,7 @@ class ConcreteMachine:
 		Cycles cycles_since_audio_update_;
 
 		ROMMachine::ROMFetcher rom_fetcher_;
-		std::unique_ptr<AppleII::Card> cards_[7];
+		std::array<std::unique_ptr<AppleII::Card>, 7> cards_;
 		Cycles cycles_since_card_update_;
 		int stretched_cycles_since_card_update_ = 0;
 
@@ -254,7 +257,7 @@ class ConcreteMachine:
 						Decode the area conventionally used by cards for ROMs:
 							0xCn00 — 0xCnff: card n.
 					*/
-					const int card_number = (address - 0xc100) >> 8;
+					const size_t card_number = (address - 0xc100) >> 8;
 					if(cards_[card_number]) {
 						update_cards();
 						cards_[card_number]->perform_bus_operation(operation, address & 0xff, value);
@@ -264,7 +267,7 @@ class ConcreteMachine:
 						Decode the area conventionally used by cards for registers:
 							C0n0--C0nF: card n - 8.
 					*/
-					const int card_number = (address - 0xc090) >> 4;
+					const size_t card_number = (address - 0xc090) >> 4;
 					if(cards_[card_number]) {
 						update_cards();
 						cards_[card_number]->perform_bus_operation(operation, 0x100 | (address&0xf), value);
@@ -372,6 +375,13 @@ class ConcreteMachine:
 			}
 			return true;
 		}
+
+		// MARK: Activity::Source
+		void set_activity_observer(Activity::Observer *observer) override {
+			for(const auto &card: cards_) {
+				if(card) card->set_activity_observer(observer);
+			}
+		}
 };
 
 }
diff --git a/Machines/AppleII/Card.hpp b/Machines/AppleII/Card.hpp
index d7c00cdb6..de89bb75e 100644
--- a/Machines/AppleII/Card.hpp
+++ b/Machines/AppleII/Card.hpp
@@ -11,6 +11,7 @@
 
 #include "../../Processors/6502/6502.hpp"
 #include "../../ClockReceiver/ClockReceiver.hpp"
+#include "../../Activity/Observer.hpp"
 
 namespace AppleII {
 
@@ -21,6 +22,9 @@ class Card {
 
 		/*! Performs a bus operation; the card is implicitly selected. */
 		virtual void perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) = 0;
+
+		/*! Supplies a target for observers. */
+		virtual void set_activity_observer(Activity::Observer *observer) {}
 };
 
 }
diff --git a/Machines/AppleII/DiskIICard.cpp b/Machines/AppleII/DiskIICard.cpp
index b0941b28e..527b23b39 100644
--- a/Machines/AppleII/DiskIICard.cpp
+++ b/Machines/AppleII/DiskIICard.cpp
@@ -40,3 +40,7 @@ void DiskIICard::run_for(Cycles cycles, int stretches) {
 void DiskIICard::set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive) {
 	diskii_.set_disk(disk, drive);
 }
+
+void DiskIICard::set_activity_observer(Activity::Observer *observer) {
+	diskii_.set_activity_observer(observer);
+}
diff --git a/Machines/AppleII/DiskIICard.hpp b/Machines/AppleII/DiskIICard.hpp
index 7f03d0ddc..849e37f94 100644
--- a/Machines/AppleII/DiskIICard.hpp
+++ b/Machines/AppleII/DiskIICard.hpp
@@ -24,8 +24,11 @@ namespace AppleII {
 class DiskIICard: public Card {
 	public:
 		DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector);
+
 		void perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) override;
 		void run_for(Cycles cycles, int stretches) override;
+		void set_activity_observer(Activity::Observer *observer) override;
+
 		void set_disk(const std::shared_ptr<Storage::Disk::Disk> &disk, int drive);
 
 	private:
diff --git a/Machines/Commodore/1540/Implementation/C1540.cpp b/Machines/Commodore/1540/Implementation/C1540.cpp
index f08deb661..ec0140f71 100644
--- a/Machines/Commodore/1540/Implementation/C1540.cpp
+++ b/Machines/Commodore/1540/Implementation/C1540.cpp
@@ -108,6 +108,11 @@ void Machine::run_for(const Cycles cycles) {
 		Storage::Disk::Controller::run_for(cycles);
 }
 
+void MachineBase::set_activity_observer(Activity::Observer *observer) {
+	drive_VIA_.bus_handler().set_activity_observer(observer);
+	drive_->set_activity_observer(observer, "Drive", false);
+}
+
 // MARK: - 6522 delegate
 
 void MachineBase::mos6522_did_change_interrupt_status(void *mos6522) {
@@ -209,8 +214,6 @@ void DriveVIA::set_delegate(Delegate *delegate) {
 }
 
 // write protect tab uncovered
-DriveVIA::DriveVIA() : port_b_(0xff), port_a_(0xff), delegate_(nullptr) {}
-
 uint8_t DriveVIA::get_port_input(MOS::MOS6522::Port port) {
 	return port ? port_b_ : port_a_;
 }
@@ -255,14 +258,22 @@ void DriveVIA::set_port_output(MOS::MOS6522::Port port, uint8_t value, uint8_t d
 				delegate_->drive_via_did_set_data_density(this, (value >> 5)&3);
 			}
 
-			// TODO: something with the drive LED
-//			printf("LED: %s\n", value&8 ? "On" : "Off");
+			// post the LED status
+			if(observer_) observer_->set_led_status("Drive", !!(value&8));
 
 			previous_port_b_output_ = value;
 		}
 	}
 }
 
+void DriveVIA::set_activity_observer(Activity::Observer *observer) {
+	observer_ = observer;
+	if(observer) {
+		observer->register_led("Drive");
+		observer->set_led_status("Drive", !!(previous_port_b_output_&8));
+	}
+}
+
 // MARK: - SerialPort
 
 void SerialPort::set_input(::Commodore::Serial::Line line, ::Commodore::Serial::LineLevel level) {
diff --git a/Machines/Commodore/1540/Implementation/C1540Base.hpp b/Machines/Commodore/1540/Implementation/C1540Base.hpp
index 3b54788e7..aef0b8686 100644
--- a/Machines/Commodore/1540/Implementation/C1540Base.hpp
+++ b/Machines/Commodore/1540/Implementation/C1540Base.hpp
@@ -14,6 +14,7 @@
 
 #include "../../SerialBus.hpp"
 
+#include "../../../../Activity/Source.hpp"
 #include "../../../../Storage/Disk/Disk.hpp"
 
 #include "../../../../Storage/Disk/Controller/DiskController.hpp"
@@ -83,8 +84,6 @@ class DriveVIA: public MOS::MOS6522::IRQDelegatePortHandler {
 		};
 		void set_delegate(Delegate *);
 
-		DriveVIA();
-
 		uint8_t get_port_input(MOS::MOS6522::Port port);
 
 		void set_sync_detected(bool);
@@ -96,12 +95,15 @@ class DriveVIA: public MOS::MOS6522::IRQDelegatePortHandler {
 
 		void set_port_output(MOS::MOS6522::Port, uint8_t value, uint8_t direction_mask);
 
+		void set_activity_observer(Activity::Observer *observer);
+
 	private:
-		uint8_t port_b_, port_a_;
-		bool should_set_overflow_;
-		bool drive_motor_;
-		uint8_t previous_port_b_output_;
-		Delegate *delegate_;
+		uint8_t port_b_ = 0xff, port_a_ = 0xff;
+		bool should_set_overflow_ = false;
+		bool drive_motor_ = false;
+		uint8_t previous_port_b_output_ = 0;
+		Delegate *delegate_ = nullptr;
+		Activity::Observer *observer_ = nullptr;
 };
 
 /*!
@@ -135,6 +137,9 @@ class MachineBase:
 		void drive_via_did_step_head(void *driveVIA, int direction);
 		void drive_via_did_set_data_density(void *driveVIA, int density);
 
+		/// Attaches the activity observer to this C1540.
+		void set_activity_observer(Activity::Observer *observer);
+
 	protected:
 		CPU::MOS6502::Processor<MachineBase, false> m6502_;
 		std::shared_ptr<Storage::Disk::Drive> drive_;
diff --git a/Machines/Commodore/Vic-20/Vic20.cpp b/Machines/Commodore/Vic-20/Vic20.cpp
index e119014bd..d2066ec9e 100644
--- a/Machines/Commodore/Vic-20/Vic20.cpp
+++ b/Machines/Commodore/Vic-20/Vic20.cpp
@@ -10,6 +10,7 @@
 
 #include "Keyboard.hpp"
 
+#include "../../../Activity/Source.hpp"
 #include "../../ConfigurationTarget.hpp"
 #include "../../CRTMachine.hpp"
 #include "../../KeyboardMachine.hpp"
@@ -303,7 +304,8 @@ class ConcreteMachine:
 	public Utility::TypeRecipient,
 	public Storage::Tape::BinaryTapePlayer::Delegate,
 	public Machine,
-	public Sleeper::SleepObserver {
+	public Sleeper::SleepObserver,
+	public Activity::Source {
 	public:
 		ConcreteMachine() :
 				m6502_(*this),
@@ -752,6 +754,11 @@ class ConcreteMachine:
 			set_use_fast_tape();
 		}
 
+		// MARK: - Activity Source
+		void set_activity_observer(Activity::Observer *observer) override {
+			if(c1540_) c1540_->set_activity_observer(observer);
+		}
+
 	private:
 		void update_video() {
 			mos6560_->run_for(cycles_since_mos6560_update_.flush());
diff --git a/Machines/DynamicMachine.hpp b/Machines/DynamicMachine.hpp
index 6d261b6da..a8f2595b5 100644
--- a/Machines/DynamicMachine.hpp
+++ b/Machines/DynamicMachine.hpp
@@ -10,6 +10,7 @@
 #define DynamicMachine_h
 
 #include "../Configurable/Configurable.hpp"
+#include "../Activity/Source.hpp"
 #include "ConfigurationTarget.hpp"
 #include "CRTMachine.hpp"
 #include "JoystickMachine.hpp"
@@ -24,6 +25,8 @@ namespace Machine {
 */
 struct DynamicMachine {
 	virtual ~DynamicMachine() {}
+
+	virtual Activity::Source *activity_source() = 0;
 	virtual ConfigurationTarget::Machine *configuration_target() = 0;
 	virtual CRTMachine::Machine *crt_machine() = 0;
 	virtual JoystickMachine::Machine *joystick_machine() = 0;
diff --git a/Machines/Electron/Electron.cpp b/Machines/Electron/Electron.cpp
index 36a2ff5fd..103c2f855 100644
--- a/Machines/Electron/Electron.cpp
+++ b/Machines/Electron/Electron.cpp
@@ -8,6 +8,7 @@
 
 #include "Electron.hpp"
 
+#include "../../Activity/Source.hpp"
 #include "../ConfigurationTarget.hpp"
 #include "../CRTMachine.hpp"
 #include "../KeyboardMachine.hpp"
@@ -45,7 +46,8 @@ class ConcreteMachine:
 	public Configurable::Device,
 	public CPU::MOS6502::BusHandler,
 	public Tape::Delegate,
-	public Utility::TypeRecipient {
+	public Utility::TypeRecipient,
+	public Activity::Source {
 	public:
 		ConcreteMachine() :
 			m6502_(*this),
@@ -215,12 +217,15 @@ class ConcreteMachine:
 
 							tape_.set_is_enabled((*value & 6) != 6);
 							tape_.set_is_in_input_mode((*value & 6) == 0);
-							tape_.set_is_running(((*value)&0x40) ? true : false);
+							tape_.set_is_running((*value & 0x40) ? true : false);
 
-							// TODO: caps lock LED
+							caps_led_state_ = !!(*value & 0x80);
+							if(activity_observer_)
+								activity_observer_->set_led_status(caps_led, caps_led_state_);
 						}
 
-					// deliberate fallthrough
+					// deliberate fallthrough; fe07 contains the display mode.
+
 					case 0xfe02: case 0xfe03:
 					case 0xfe08: case 0xfe09: case 0xfe0a: case 0xfe0b:
 					case 0xfe0c: case 0xfe0d: case 0xfe0e: case 0xfe0f:
@@ -322,8 +327,6 @@ class ConcreteMachine:
 										if(!ram_[0x247] && service_call == 14) {
 											tape_.set_delegate(nullptr);
 
-											// TODO: handle tape wrap around.
-
 											int cycles_left_while_plausibly_in_data = 50;
 											tape_.clear_interrupts(Interrupt::ReceiveDataFull);
 											while(!tape_.get_tape()->is_at_end()) {
@@ -477,6 +480,14 @@ class ConcreteMachine:
 			return selection_set;
 		}
 
+		void set_activity_observer(Activity::Observer *observer) override {
+			activity_observer_ = observer;
+			if(activity_observer_) {
+				activity_observer_->register_led(caps_led);
+				activity_observer_->set_led_status(caps_led, caps_led_state_);
+			}
+		}
+
 	private:
 		// MARK: - Work deferral updates.
 		inline void update_display() {
@@ -563,6 +574,11 @@ class ConcreteMachine:
 		Outputs::Speaker::LowpassSpeaker<SoundGenerator> speaker_;
 
 		bool speaker_is_enabled_ = false;
+
+		// MARK: - Caps Lock status and the activity observer.
+		const std::string caps_led = "CAPS";
+		bool caps_led_state_ = false;
+		Activity::Observer *activity_observer_ = nullptr;
 };
 
 }
diff --git a/Machines/Electron/Plus3.cpp b/Machines/Electron/Plus3.cpp
index 95178291a..1338bd11d 100644
--- a/Machines/Electron/Plus3.cpp
+++ b/Machines/Electron/Plus3.cpp
@@ -53,3 +53,8 @@ void Plus3::set_motor_on(bool on) {
 	// writing state, so plenty of work to do in general here.
 	get_drive().set_motor_on(on);
 }
+
+void Plus3::set_activity_observer(Activity::Observer *observer) {
+	drives_[0]->set_activity_observer(observer, "Drive 1", true);
+	drives_[1]->set_activity_observer(observer, "Drive 2", true);
+}
diff --git a/Machines/Electron/Plus3.hpp b/Machines/Electron/Plus3.hpp
index 71b8b1c4b..d5bf1a7c9 100644
--- a/Machines/Electron/Plus3.hpp
+++ b/Machines/Electron/Plus3.hpp
@@ -10,6 +10,7 @@
 #define Plus3_hpp
 
 #include "../../Components/1770/1770.hpp"
+#include "../../Activity/Observer.hpp"
 
 namespace Electron {
 
@@ -19,6 +20,7 @@ class Plus3 : public WD::WD1770 {
 
 		void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive);
 		void set_control_register(uint8_t control);
+		void set_activity_observer(Activity::Observer *observer);
 
 	private:
 		void set_control_register(uint8_t control, uint8_t changes);
diff --git a/Machines/MSX/DiskROM.cpp b/Machines/MSX/DiskROM.cpp
index 2025098b9..af414657a 100644
--- a/Machines/MSX/DiskROM.cpp
+++ b/Machines/MSX/DiskROM.cpp
@@ -56,11 +56,12 @@ void DiskROM::run_for(HalfCycles half_cycles) {
 	controller_cycles_ %= 715909;
 }
 
-void DiskROM::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
+void DiskROM::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
 	if(!drives_[drive]) {
 		drives_[drive].reset(new Storage::Disk::Drive(8000000, 300, 2));
 		drives_[drive]->set_head(selected_head_);
 		if(drive == selected_drive_) set_drive(drives_[drive]);
+		drives_[drive]->set_activity_observer(observer_, drive_name(drive), true);
 	}
 	drives_[drive]->set_disk(disk);
 }
@@ -69,3 +70,16 @@ void DiskROM::set_head_load_request(bool head_load) {
 	// Magic!
 	set_head_loaded(head_load);
 }
+
+void DiskROM::set_activity_observer(Activity::Observer *observer) {
+	size_t c = 0;
+	observer_ = observer;
+	for(auto &drive: drives_) {
+		if(drive) drive->set_activity_observer(observer, drive_name(c), true);
+		++c;
+	}
+}
+
+std::string DiskROM::drive_name(size_t index) {
+	return "Drive " + std::to_string(index);
+}
diff --git a/Machines/MSX/DiskROM.hpp b/Machines/MSX/DiskROM.hpp
index bab03d380..b3f56d92d 100644
--- a/Machines/MSX/DiskROM.hpp
+++ b/Machines/MSX/DiskROM.hpp
@@ -11,9 +11,12 @@
 
 #include "ROMSlotHandler.hpp"
 
+#include "../../Activity/Source.hpp"
 #include "../../Components/1770/1770.hpp"
 
+#include <array>
 #include <cstdint>
+#include <string>
 #include <vector>
 
 namespace MSX {
@@ -26,17 +29,20 @@ class DiskROM: public ROMSlotHandler, public WD::WD1770 {
 		uint8_t read(uint16_t address) override;
 		void run_for(HalfCycles half_cycles) override;
 
-		void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive);
+		void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
+		void set_activity_observer(Activity::Observer *observer);
 
 	private:
 		const std::vector<uint8_t> &rom_;
 
 		long int controller_cycles_ = 0;
-		int selected_drive_ = 0;
+		size_t selected_drive_ = 0;
 		int selected_head_ = 0;
-		std::shared_ptr<Storage::Disk::Drive> drives_[4];
+		std::array<std::shared_ptr<Storage::Disk::Drive>, 2> drives_;
 
 		void set_head_load_request(bool head_load) override;
+		std::string drive_name(size_t index);
+		Activity::Observer *observer_ = nullptr;
 };
 
 }
diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp
index 6b57f714c..daed39b44 100644
--- a/Machines/MSX/MSX.cpp
+++ b/Machines/MSX/MSX.cpp
@@ -30,6 +30,7 @@
 #include "../../Storage/Tape/Parsers/MSX.hpp"
 #include "../../Storage/Tape/Tape.hpp"
 
+#include "../../Activity/Source.hpp"
 #include "../CRTMachine.hpp"
 #include "../ConfigurationTarget.hpp"
 #include "../KeyboardMachine.hpp"
@@ -87,7 +88,8 @@ class ConcreteMachine:
 	public KeyboardMachine::Machine,
 	public Configurable::Device,
 	public MemoryMap,
-	public Sleeper::SleepObserver {
+	public Sleeper::SleepObserver,
+	public Activity::Source {
 	public:
 		ConcreteMachine():
 			z80_(*this),
@@ -200,12 +202,14 @@ class ConcreteMachine:
 			}
 
 			if(!media.disks.empty()) {
-				DiskROM *disk_rom = dynamic_cast<DiskROM *>(memory_slots_[2].handler.get());
-				int drive = 0;
-				for(auto &disk : media.disks) {
-					disk_rom->set_disk(disk, drive);
-					drive++;
-					if(drive == 2) break;
+				DiskROM *disk_rom = get_disk_rom();
+				if(disk_rom) {
+					size_t drive = 0;
+					for(auto &disk : media.disks) {
+						disk_rom->set_disk(disk, drive);
+						drive++;
+						if(drive == 2) break;
+					}
 				}
 			}
 
@@ -556,7 +560,18 @@ class ConcreteMachine:
 			set_use_fast_tape();
 		}
 
+		// MARK: - Activity::Source
+		void set_activity_observer(Activity::Observer *observer) override {
+			DiskROM *disk_rom = get_disk_rom();
+			if(disk_rom) {
+				disk_rom->set_activity_observer(observer);
+			}
+		}
+
 	private:
+		DiskROM *get_disk_rom() {
+			return dynamic_cast<DiskROM *>(memory_slots_[2].handler.get());
+		}
 		void update_audio() {
 			speaker_.run_for(audio_queue_, time_since_ay_update_.divide_cycles(Cycles(2)));
 		}
diff --git a/Machines/Oric/Microdisc.cpp b/Machines/Oric/Microdisc.cpp
index a9701967e..e040ce5fb 100644
--- a/Machines/Oric/Microdisc.cpp
+++ b/Machines/Oric/Microdisc.cpp
@@ -22,10 +22,11 @@ Microdisc::Microdisc() : WD1770(P1793) {
 	set_control_register(last_control_, 0xff);
 }
 
-void Microdisc::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive) {
+void Microdisc::set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive) {
 	if(!drives_[drive]) {
 		drives_[drive].reset(new Storage::Disk::Drive(8000000, 300, 2));
 		if(drive == selected_drive_) set_drive(drives_[drive]);
+		drives_[drive]->set_activity_observer(observer_, drive_name(drive), false);
 	}
 	drives_[drive]->set_disk(disk);
 }
@@ -48,8 +49,8 @@ void Microdisc::set_control_register(uint8_t control, uint8_t changes) {
 	// b4: side select
 	if(changes & 0x10) {
 		int head = (control & 0x10) ? 1 : 0;
-		for(int c = 0; c < 4; c++) {
-			if(drives_[c]) drives_[c]->set_head(head);
+		for(auto &drive : drives_) {
+			if(drive) drive->set_head(head);
 		}
 	}
 
@@ -89,10 +90,12 @@ uint8_t Microdisc::get_data_request_register() {
 }
 
 void Microdisc::set_head_load_request(bool head_load) {
+	head_load_request_ = head_load;
+
 	// The drive motors (at present: I believe **all drive motors** regardless of the selected drive) receive
 	// the current head load request state.
-	for(int c = 0; c < 4; c++) {
-		if(drives_[c]) drives_[c]->set_motor_on(head_load);
+	for(auto &drive : drives_) {
+		if(drive) drive->set_motor_on(head_load);
 	}
 
 	// A request to load the head results in a delay until the head is confirmed loaded. This delay is handled
@@ -103,6 +106,10 @@ void Microdisc::set_head_load_request(bool head_load) {
 		head_load_request_counter_ = head_load_request_counter_target;
 		set_head_loaded(head_load);
 	}
+
+	if(observer_) {
+		observer_->set_led_status("Microdisc", head_load);
+	}
 }
 
 void Microdisc::run_for(const Cycles cycles) {
@@ -116,3 +123,20 @@ void Microdisc::run_for(const Cycles cycles) {
 bool Microdisc::get_drive_is_ready() {
 	return true;
 }
+
+void Microdisc::set_activity_observer(Activity::Observer *observer) {
+	observer_ = observer;
+	if(observer) {
+		observer->register_led("Microdisc");
+		observer_->set_led_status("Microdisc", head_load_request_);
+	}
+	size_t c = 0;
+	for(auto &drive : drives_) {
+		if(drive) drive->set_activity_observer(observer, drive_name(c), false);
+		++c;
+	}
+}
+
+std::string Microdisc::drive_name(size_t index) {
+	return "Drive " + std::to_string(index);
+}
diff --git a/Machines/Oric/Microdisc.hpp b/Machines/Oric/Microdisc.hpp
index 1ea325dc7..862e7ec8e 100644
--- a/Machines/Oric/Microdisc.hpp
+++ b/Machines/Oric/Microdisc.hpp
@@ -10,6 +10,9 @@
 #define Microdisc_hpp
 
 #include "../../Components/1770/1770.hpp"
+#include "../../Activity/Observer.hpp"
+
+#include <array>
 
 namespace Oric {
 
@@ -17,7 +20,7 @@ class Microdisc: public WD::WD1770 {
 	public:
 		Microdisc();
 
-		void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, int drive);
+		void set_disk(std::shared_ptr<Storage::Disk::Disk> disk, size_t drive);
 		void set_control_register(uint8_t control);
 		uint8_t get_interrupt_request_register();
 		uint8_t get_data_request_register();
@@ -39,17 +42,23 @@ class Microdisc: public WD::WD1770 {
 		inline void set_delegate(Delegate *delegate)	{	delegate_ = delegate;	WD1770::set_delegate(delegate);	}
 		inline int get_paging_flags()					{	return paging_flags_;									}
 
+		void set_activity_observer(Activity::Observer *observer);
+
 	private:
 		void set_control_register(uint8_t control, uint8_t changes);
 		void set_head_load_request(bool head_load);
 		bool get_drive_is_ready();
-		std::shared_ptr<Storage::Disk::Drive> drives_[4];
-		int selected_drive_;
+		std::array<std::shared_ptr<Storage::Disk::Drive>, 4> drives_;
+		size_t selected_drive_;
 		bool irq_enable_ = false;
 		int paging_flags_ = BASICDisable;
 		int head_load_request_counter_ = -1;
+		bool head_load_request_ = false;
 		Delegate *delegate_ = nullptr;
 		uint8_t last_control_ = 0;
+		Activity::Observer *observer_ = nullptr;
+
+		std::string drive_name(size_t index);
 };
 
 }
diff --git a/Machines/Oric/Oric.cpp b/Machines/Oric/Oric.cpp
index c5a8492a9..b1d8b0a2e 100644
--- a/Machines/Oric/Oric.cpp
+++ b/Machines/Oric/Oric.cpp
@@ -12,6 +12,7 @@
 #include "Microdisc.hpp"
 #include "Video.hpp"
 
+#include "../../Activity/Source.hpp"
 #include "../ConfigurationTarget.hpp"
 #include "../CRTMachine.hpp"
 #include "../KeyboardMachine.hpp"
@@ -200,6 +201,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
 	public Utility::TypeRecipient,
 	public Storage::Tape::BinaryTapePlayer::Delegate,
 	public Microdisc::Delegate,
+	public Activity::Source,
 	public Machine {
 
 	public:
@@ -329,10 +331,10 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
 				switch(disk_interface) {
 					case Analyser::Static::Oric::Target::DiskInterface::Microdisc: {
 						inserted = true;
-						int drive_index = 0;
+						size_t drive_index = 0;
 						for(auto &disk : media.disks) {
 							if(drive_index < 4) microdisc_.set_disk(disk, drive_index);
-							drive_index++;
+							++drive_index;
 						}
 					} break;
 					case Analyser::Static::Oric::Target::DiskInterface::Pravetz: {
@@ -340,7 +342,7 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
 						int drive_index = 0;
 						for(auto &disk : media.disks) {
 							if(drive_index < 2) diskii_.set_disk(disk, drive_index);
-							drive_index++;
+							++drive_index;
 						}
 					} break;
 
@@ -551,6 +553,18 @@ template <Analyser::Static::Oric::Target::DiskInterface disk_interface> class Co
 			return selection_set;
 		}
 
+		void set_activity_observer(Activity::Observer *observer) override {
+			switch(disk_interface) {
+				default: break;
+				case Analyser::Static::Oric::Target::DiskInterface::Microdisc:
+					microdisc_.set_activity_observer(observer);
+				break;
+				case Analyser::Static::Oric::Target::DiskInterface::Pravetz:
+					diskii_.set_activity_observer(observer);
+				break;
+			}
+		}
+
 	private:
 		const uint16_t basic_invisible_ram_top_ = 0xffff;
 		const uint16_t basic_visible_ram_top_ = 0xbfff;
diff --git a/Machines/Utility/TypedDynamicMachine.hpp b/Machines/Utility/TypedDynamicMachine.hpp
index d0ecc70c8..db3632733 100644
--- a/Machines/Utility/TypedDynamicMachine.hpp
+++ b/Machines/Utility/TypedDynamicMachine.hpp
@@ -25,6 +25,10 @@ template<typename T> class TypedDynamicMachine: public ::Machine::DynamicMachine
 			return *this;
 		}
 
+		Activity::Source *activity_source() override {
+			return get<Activity::Source>();
+		}
+
 		ConfigurationTarget::Machine *configuration_target() override {
 			return get<ConfigurationTarget::Machine>();
 		}
diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj
index 6cfd56378..cab4b4feb 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj	
@@ -863,6 +863,8 @@
 		4B5073051DDD3B9400C48FBD /* ArrayBuilder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = ArrayBuilder.cpp; sourceTree = "<group>"; };
 		4B5073061DDD3B9400C48FBD /* ArrayBuilder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = ArrayBuilder.hpp; sourceTree = "<group>"; };
 		4B5073091DDFCFDF00C48FBD /* ArrayBuilderTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ArrayBuilderTests.mm; sourceTree = "<group>"; };
+		4B51F70920A521D700AFA2C1 /* Source.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Source.hpp; sourceTree = "<group>"; };
+		4B51F70A20A521D700AFA2C1 /* Observer.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Observer.hpp; sourceTree = "<group>"; };
 		4B54C0BB1F8D8E790050900F /* KeyboardMachine.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = KeyboardMachine.cpp; sourceTree = "<group>"; };
 		4B54C0BD1F8D8F450050900F /* Keyboard.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = Keyboard.cpp; path = Oric/Keyboard.cpp; sourceTree = "<group>"; };
 		4B54C0BE1F8D8F450050900F /* Keyboard.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = Keyboard.hpp; path = Oric/Keyboard.hpp; sourceTree = "<group>"; };
@@ -1961,6 +1963,16 @@
 			path = 1540;
 			sourceTree = "<group>";
 		};
+		4B51F70820A521D700AFA2C1 /* Activity */ = {
+			isa = PBXGroup;
+			children = (
+				4B51F70920A521D700AFA2C1 /* Source.hpp */,
+				4B51F70A20A521D700AFA2C1 /* Observer.hpp */,
+			);
+			name = Activity;
+			path = ../../Activity;
+			sourceTree = "<group>";
+		};
 		4B55CE551C3B7D360093A61B /* Documents */ = {
 			isa = PBXGroup;
 			children = (
@@ -2697,6 +2709,7 @@
 			isa = PBXGroup;
 			children = (
 				4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */,
+				4B51F70820A521D700AFA2C1 /* Activity */,
 				4B8944E2201967B4007DE474 /* Analyser */,
 				4BB73EA01B587A5100552FC2 /* Clock Signal */,
 				4BB73EB51B587A5100552FC2 /* Clock SignalTests */,
diff --git a/Storage/Disk/Drive.cpp b/Storage/Disk/Drive.cpp
index d62654171..8418aa1ff 100644
--- a/Storage/Disk/Drive.cpp
+++ b/Storage/Disk/Drive.cpp
@@ -50,7 +50,12 @@ bool Drive::get_is_track_zero() {
 void Drive::step(HeadPosition offset) {
 	HeadPosition old_head_position = head_position_;
 	head_position_ += offset;
-	if(head_position_ < HeadPosition(0)) head_position_ = HeadPosition(0);
+	if(head_position_ < HeadPosition(0)) {
+		head_position_ = HeadPosition(0);
+		if(observer_) observer_->announce_drive_event(drive_name_, Activity::Observer::DriveEvent::StepBelowZero);
+	} else {
+		if(observer_) observer_->announce_drive_event(drive_name_, Activity::Observer::DriveEvent::StepNormal);
+	}
 
 	// If the head moved, flush the old track.
 	if(head_position_ != old_head_position) {
@@ -88,6 +93,14 @@ bool Drive::get_is_ready() {
 
 void Drive::set_motor_on(bool motor_is_on) {
 	motor_is_on_ = motor_is_on;
+
+	if(observer_) {
+		observer_->set_drive_motor_status(drive_name_, motor_is_on_);
+		if(announce_motor_led_) {
+			observer_->set_led_status(drive_name_, motor_is_on_);
+		}
+	}
+
 	if(!motor_is_on) {
 		ready_index_count_ = 0;
 		if(disk_) disk_->flush_tracks();
@@ -265,3 +278,19 @@ void Drive::end_writing() {
 		invalidate_track();
 	}
 }
+
+void Drive::set_activity_observer(Activity::Observer *observer, const std::string &name, bool add_motor_led) {
+	observer_ = observer;
+	announce_motor_led_ = add_motor_led;
+	if(observer) {
+		drive_name_ = name;
+
+		observer->register_drive(drive_name_);
+		observer->set_drive_motor_status(drive_name_, motor_is_on_);
+
+		if(add_motor_led) {
+			observer->register_led(drive_name_);
+			observer->set_led_status(drive_name_, motor_is_on_);
+		}
+	}
+}
diff --git a/Storage/Disk/Drive.hpp b/Storage/Disk/Drive.hpp
index be32071e7..e10ab4099 100644
--- a/Storage/Disk/Drive.hpp
+++ b/Storage/Disk/Drive.hpp
@@ -14,6 +14,7 @@
 #include "Track/PCMPatchedTrack.hpp"
 
 #include "../TimedEventLoop.hpp"
+#include "../../Activity/Observer.hpp"
 #include "../../ClockReceiver/Sleeper.hpp"
 
 #include <memory>
@@ -122,6 +123,10 @@ class Drive: public Sleeper, public TimedEventLoop {
 		// As per Sleeper.
 		bool is_sleeping();
 
+		/// Adds an activity observer; it'll be notified of disk activity.
+		/// The caller can specify whether to add an LED based on disk motor.
+		void set_activity_observer(Activity::Observer *observer, const std::string &name, bool add_motor_led);
+
 	private:
 		// Drives contain an entire disk; from that a certain track
 		// will be currently under the head.
@@ -189,6 +194,11 @@ class Drive: public Sleeper, public TimedEventLoop {
 
 		void setup_track();
 		void invalidate_track();
+
+		// Activity observer description.
+		Activity::Observer *observer_ = nullptr;
+		std::string drive_name_;
+		bool announce_motor_led_ = false;
 };