Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions sw/minihil/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,16 @@ include_directories(include)

# Source files (explicitly declared)
set(SOURCES
src/main.cpp
src/network/tcp_server.cpp
src/protocol/jsonrpc_router.cpp
src/main.cc
src/network/tcp_server.cc
src/protocol/jsonrpc_router.cc
src/core/base_relay_controller.cc
)

if(GPIODCXX_FOUND)
list(APPEND SOURCES src/hardware/gpiod_relay_controller.cpp)
list(APPEND SOURCES src/hardware/gpiod_relay_controller.cc)
else()
list(APPEND SOURCES src/sil/sil_relay_controller.cpp)
list(APPEND SOURCES src/sil/sil_relay_controller.cc)
endif()

# Executable
Expand Down
64 changes: 64 additions & 0 deletions sw/minihil/include/core/base_relay_controller.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#pragma once

#include "core/idevice_controller.h"
#include <map>
#include <mutex>
#include <thread>
#include <atomic>
#include <chrono>
#include <string>

namespace minihil {

class BaseRelayController : public IDeviceController {
public:
BaseRelayController();
~BaseRelayController() override;

bool init() override;
bool setRelay(int relayId, bool state) override;
bool getRelay(int relayId) const override;
std::map<int, bool> getAllStates() const override;

// Replicating microhil_base modes
bool startTimer(int relayId, uint32_t seconds) override;
bool startPulse(int relayId, uint32_t durationMs) override;
bool startBlink(int relayId, uint32_t onMs, uint32_t offMs, uint32_t count) override;
std::string getRelayStatus(int relayId) const override;

protected:
// Pure virtual methods to be implemented by Gpiod/Sil controllers for actual IO
virtual bool initHardware() = 0;
virtual bool setRelayPhysical(int relayId, bool state) = 0;

private:
enum class RelayMode {
TOGGLE,
TIMER,
PULSE,
BLINK
};

struct RelayState {
RelayMode mode = RelayMode::TOGGLE;
bool active = false;
bool state = false;
std::chrono::steady_clock::time_point startTime;
uint64_t durationMs = 0;
uint32_t blinkOnMs = 0;
uint32_t blinkOffMs = 0;
uint32_t blinkCount = 0;
bool blinkPhase = false;
};

void tickLoop();
void tick();
void setRelayInternal(int relayId, bool state);

mutable std::mutex m_mutex;
std::map<int, RelayState> m_relayStates;
std::thread m_tickThread;
std::atomic<bool> m_running{false};
};

} // namespace minihil
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once
#include <map>
#include <string>
#include <stdint.h>

namespace minihil {

Expand All @@ -18,6 +20,12 @@ class IDeviceController {

// Returns a map of all relay channels and their states
virtual std::map<int, bool> getAllStates() const = 0;

// Replicating microhil_base modes
virtual bool startTimer(int relayId, uint32_t seconds) = 0;
virtual bool startPulse(int relayId, uint32_t durationMs) = 0;
virtual bool startBlink(int relayId, uint32_t onMs, uint32_t offMs, uint32_t count) = 0;
virtual std::string getRelayStatus(int relayId) const = 0;
};

} // namespace minihil
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
#pragma once

#include "core/idevice_controller.hpp"
#include "core/base_relay_controller.h"
#include <string>
#include <vector>
#include <map>
#include <memory>

namespace minihil {

class GpiodRelayController : public IDeviceController {
class GpiodRelayController : public BaseRelayController {
public:
GpiodRelayController();
~GpiodRelayController() override;

bool init() override;
bool setRelay(int relayId, bool state) override;
bool getRelay(int relayId) const override;
std::map<int, bool> getAllStates() const override;
protected:
bool initHardware() override;
bool setRelayPhysical(int relayId, bool state) override;

private:
std::string m_chipPath;
// Map from Relay ID (1-8) to BCM pin offset
std::map<int, unsigned int> m_relayGpioMap;
// Cache for relay states
mutable std::map<int, bool> m_states;

struct Impl;
std::unique_ptr<Impl> m_impl;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#pragma once

#include "core/iserver.hpp"
#include "core/irpc_handler.hpp"
#include "core/iserver.h"
#include "core/irpc_handler.h"
#include <memory>
#include <atomic>
#include <thread>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#pragma once

#include "core/irpc_handler.hpp"
#include "core/irpc_handler.h"
#include <map>
#include <string>
#include <functional>
Expand Down
17 changes: 17 additions & 0 deletions sw/minihil/include/sil/sil_relay_controller.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#include "core/base_relay_controller.h"

namespace minihil {

class SilRelayController : public BaseRelayController {
public:
SilRelayController();
~SilRelayController() override;

protected:
bool initHardware() override;
bool setRelayPhysical(int relayId, bool state) override;
};

} // namespace minihil
22 changes: 0 additions & 22 deletions sw/minihil/include/sil/sil_relay_controller.hpp

This file was deleted.

150 changes: 150 additions & 0 deletions sw/minihil/scripts/test_channels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import os
import subprocess
import socket
import json
import time
import sys

def send_rpc(sock, method, params=None):
req = {
"jsonrpc": "2.0",
"method": method,
"id": 1
}
if params is not None:
req["params"] = params
payload = json.dumps(req) + "\n"
sock.sendall(payload.encode('utf-8'))

# Read response until newline
resp_bytes = b''
while b'\n' not in resp_bytes:
chunk = sock.recv(1)
if not chunk:
break
resp_bytes += chunk

resp_str = resp_bytes.decode('utf-8').strip()
if not resp_str:
return None
return json.loads(resp_str)

def main():
script_dir = os.path.dirname(os.path.realpath(__file__))
daemon_path = os.path.abspath(os.path.join(script_dir, "..", "build", "minihild"))

print(f"Spawning daemon from: {daemon_path}")
proc = subprocess.Popen([daemon_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

# Wait for startup
time.sleep(1.0)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect(("localhost", 9000))
print("Connected to minihild.")

for relay_id in range(1, 9):
print(f"\n--- Testing Relay Channel {relay_id} ---")

# Test 1: Toggle Mode (set_relay)
print(f"[{relay_id}] Testing set_relay (Toggle mode)...")
res = send_rpc(sock, "set_relay", {"relay_id": relay_id, "state": True})
assert res["result"]["success"] is True
res = send_rpc(sock, "get_relays")
assert res["result"][str(relay_id)] is True

res = send_rpc(sock, "get_relay_status", {"relay_id": relay_id})
assert f"Channel {relay_id}: ON (Toggle)" in res["result"]["status"]

res = send_rpc(sock, "set_relay", {"relay_id": relay_id, "state": False})
assert res["result"]["success"] is True
res = send_rpc(sock, "get_relays")
assert res["result"][str(relay_id)] is False

# Test 2: Timer Mode (start_timer)
print(f"[{relay_id}] Testing start_timer...")
res = send_rpc(sock, "start_timer", {"relay_id": relay_id, "seconds": 2})
assert res["result"]["success"] is True

res = send_rpc(sock, "get_relays")
assert res["result"][str(relay_id)] is True

res = send_rpc(sock, "get_relay_status", {"relay_id": relay_id})
assert f"Channel {relay_id}: ON (Timer, rem:" in res["result"]["status"]

print(f"[{relay_id}] Waiting 2.5 seconds for timer to expire...")
time.sleep(2.5)

res = send_rpc(sock, "get_relays")
assert res["result"][str(relay_id)] is False

res = send_rpc(sock, "get_relay_status", {"relay_id": relay_id})
assert f"Channel {relay_id}: OFF (Toggle)" in res["result"]["status"]

# Test 3: Pulse Mode (start_pulse)
print(f"[{relay_id}] Testing start_pulse...")
res = send_rpc(sock, "start_pulse", {"relay_id": relay_id, "duration_ms": 500})
assert res["result"]["success"] is True

res = send_rpc(sock, "get_relays")
assert res["result"][str(relay_id)] is True

res = send_rpc(sock, "get_relay_status", {"relay_id": relay_id})
assert f"Channel {relay_id}: ON (Pulse, rem:" in res["result"]["status"]

print(f"[{relay_id}] Waiting 0.7 seconds for pulse to expire...")
time.sleep(0.7)

res = send_rpc(sock, "get_relays")
assert res["result"][str(relay_id)] is False

# Test 4: Blink Mode (start_blink)
print(f"[{relay_id}] Testing start_blink...")
res = send_rpc(sock, "start_blink", {"relay_id": relay_id, "on_ms": 150, "off_ms": 150, "count": 2})
assert res["result"]["success"] is True

res = send_rpc(sock, "get_relays")
assert res["result"][str(relay_id)] is True

res = send_rpc(sock, "get_relay_status", {"relay_id": relay_id})
assert f"Channel {relay_id}: ON (Blink, count: 2, phase: ON)" in res["result"]["status"]

print(f"[{relay_id}] Waiting 0.2 seconds (should transition to blink OFF phase)...")
time.sleep(0.2)
res = send_rpc(sock, "get_relays")
assert res["result"][str(relay_id)] is False
res = send_rpc(sock, "get_relay_status", {"relay_id": relay_id})
assert f"Channel {relay_id}: OFF (Blink, count: 2, phase: OFF)" in res["result"]["status"]

print(f"[{relay_id}] Waiting 0.2 seconds (should transition to blink ON phase)...")
time.sleep(0.2)
res = send_rpc(sock, "get_relays")
assert res["result"][str(relay_id)] is True
res = send_rpc(sock, "get_relay_status", {"relay_id": relay_id})
assert f"Channel {relay_id}: ON (Blink, count: 1, phase: ON)" in res["result"]["status"]

print(f"[{relay_id}] Waiting 0.4 seconds (should finish blink)...")
time.sleep(0.4)
res = send_rpc(sock, "get_relays")
assert res["result"][str(relay_id)] is False
res = send_rpc(sock, "get_relay_status", {"relay_id": relay_id})
assert f"Channel {relay_id}: OFF (Toggle)" in res["result"]["status"]

print("\nAll tests passed successfully for all 8 channels!")

except Exception as e:
print(f"Test failed: {e}")
# Print stdout/stderr of daemon to debug
proc.terminate()
stdout, stderr = proc.communicate()
print(f"Daemon stdout:\n{stdout}")
print(f"Daemon stderr:\n{stderr}")
sys.exit(1)
finally:
sock.close()
proc.terminate()
proc.wait()

if __name__ == '__main__':
main()
Loading
Loading