Daemon to allow controlling different fiber optical switches through a unified HTTP-based REST API.
  • Python 97.5%
  • Dockerfile 2.5%
Find a file
Paul Spooren 0c332a7a42 fix: fix logging and show channel switches
Signed-off-by: Paul Spooren <mail@aparcar.org>
2026-02-05 20:03:49 +01:00
.forgejo/workflows ci: add basic tests 2026-01-07 18:48:00 +07:00
fosd fix: fix logging and show channel switches 2026-02-05 20:03:49 +01:00
misc/containerlab feat: add containerlab bridge 2026-01-15 08:32:49 +07:00
tests meta: switch to uv and fosd/ folder 2026-01-07 18:43:59 +07:00
.gitignore Initial commit 2025-04-29 11:12:07 +00:00
config.example.yaml refactoring: make this a modular daemon with a REST API 2025-10-21 10:13:12 +02:00
LICENSE chore: specifiy license to EUPL-1.2 2026-01-07 11:45:42 +01:00
pyproject.toml chore: add licensing info to pyproject.toml 2026-01-07 13:14:41 +01:00
README.md feat: add containerlab bridge 2026-01-15 08:32:49 +07:00

fosd - Fiber Optic Switch Daemon

A lightweight HTTP API server for controlling fiber optic switches (FOS) from various manufacturers. The daemon provides a unified REST API interface for managing multiple fiber optic switches through a plugin-based driver architecture.

Features

  • RESTful HTTP API - Simple JSON-based API for switch control
  • Multi-switch support - Manage multiple switches simultaneously
  • Plugin driver architecture - Extensible driver system for different switch models
  • YAML configuration - Easy-to-manage configuration files
  • Asynchronous operation - Built on aiohttp for high performance

Supported Hardware

Currently Supported Switches

  • Weinert EOL Switches (UART interface)

    • Auto-detection of input/output configuration (e.g., 1x2, 1x4, etc.)
    • Firmware version reporting
    • 1-based channel indexing
    • Compatible ID: weinert-eol,uart
  • Agiltron FFSW Switches (USB/Serial)

    • Support for MEMS-based switches
    • 0-based channel indexing
    • Compatible ID: agiltron-ffsw
  • Virtual Switch (Testing/Development)

    • Simulated switch for testing
    • 0-based channel indexing (0 to N-1)
    • Compatible ID: virtual
  • Containerlab Bridge (Linux Bridge/ebtables Simulation)

    • Simulates 1:N fiber optical switch using Linux bridges
    • Designed for containerlab network topologies
    • Uses ebtables for Layer 2 switching control
    • 0-based channel indexing (0 to N-1)
    • Compatible IDs: containerlab-bridge, clab-bridge, linux-bridge-fos

Planned Support

  • Weinert EOL Switches (I2C interface) - weinert-eol,i2c

Installation

Prerequisites

  • Python 3.10 or higher
  • uv - Fast Python package installer

Install uv

# macOS and Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows
powershell -c "irm https://astral.sh/uv/install.sh | iex"

# Or with pip
pip install uv

Install Dependencies

uv sync

This will install all required packages from pyproject.toml:

  • aiohttp - Async HTTP server
  • pyserial - Serial port communication
  • pyyaml - YAML configuration parsing

Running the Daemon

# With default config location (/etc/fosd/config.yaml)
uv run fosd

# With custom config file
uv run fosd --config /path/to/config.yaml

# With debug logging
uv run fosd --loglevel DEBUG

Command Line Options

  • --config, -c - Path to configuration file (default: /etc/fosd/config.yaml)
  • --loglevel - Logging level: DEBUG, INFO, WARN, ERROR (default: INFO)

Configuration

Configuration is done via a YAML file. See config.example.yaml for a template.

Example Configuration

http:
  host: 0.0.0.0
  port: 9000

switches:
  switch_1:
    compatible: weinert-eol,uart
    port: /dev/tty.usbserial-211240
    baud: 57600

  switch_2:
    compatible: virtual
    channels: 4

  lab_switch:
    compatible: agiltron-ffsw
    port: /dev/serial/by-id/usb-FOSI-12345

  # Containerlab bridge simulation (for use in containerlab topologies)
  fiber_sim:
    compatible: containerlab-bridge
    input_interface: eth1
    output_interfaces:
      - eth2
      - eth3
      - eth4
      - eth5

Configuration Options

HTTP Server Settings

  • http.host - Bind address (e.g., 0.0.0.0 for all interfaces, 127.0.0.1 for localhost)
  • http.port - Port number (default: 9000)

Switch Configuration

Each switch is defined under the switches section with a unique identifier. Common parameters:

  • compatible - Driver identifier (see Supported Hardware section)
  • port - Serial port path (for UART/USB devices)
  • baud - Baud rate for serial communication (e.g., 57600, 115200)
  • channels - Number of output channels (for virtual switches)
  • SN - Serial number (for Agiltron switches)
  • input_interface - Input interface name (for containerlab-bridge)
  • output_interfaces - List of output interface names (for containerlab-bridge)
  • bridge_name - Linux bridge name (for containerlab-bridge, default: br-fos)

API Documentation

The daemon exposes a REST API on the configured host and port.

API Version

GET /v1

Get API version information.

Response:

{
  "version": "1.0.0"
}

List All Switches

GET /v1/switches

Get a list of all configured switches.

Response:

{
  "switches": ["switch_1", "switch_2", "lab_switch"]
}

Get Switch Information

GET /v1/switches/{id}

Get detailed information about a specific switch.

Parameters:

  • {id} - Switch identifier from configuration

Response Example (Weinert EOL):

{
  "model": "1x4",
  "firmware": "v2.1",
  "inputs": "1",
  "outputs": "4"
}

Response Example (Virtual):

{
  "name": "Virtual Fiber-Optical Switch",
  "inputs": 1,
  "outputs": 4
}

Get Current Output Channel

GET /v1/switches/{id}/output

Get the currently active output channel.

Parameters:

  • {id} - Switch identifier from configuration

Response:

{
  "channel": 2
}

Note: Channel numbering depends on the driver. Virtual switches use 0-based indexing (0 to N-1), while hardware switches may use 1-based indexing (1 to N). Check the driver documentation for your specific hardware.

Set Output Channel

PUT /v1/switches/{id}/output

Switch to a different output channel.

Parameters:

  • {id} - Switch identifier from configuration

Request Body:

{
  "channel": 2
}

Note: For virtual switches with 4 channels, valid values are 0-3. For hardware switches, consult the driver documentation.

Response:

  • 200 OK - Channel switched successfully
  • 400 Bad Request - Invalid request
  • 500 Internal Server Error - Failed to switch channel

Usage Examples

Using curl

# Get API version
curl http://localhost:9000/v1

# List all switches
curl http://localhost:9000/v1/switches

# Get switch information
curl http://localhost:9000/v1/switches/switch_1

# Get current output channel
curl http://localhost:9000/v1/switches/switch_1/output

# Set output channel to 2 (for virtual switch, valid: 0-3 if 4 channels)
curl -X PUT http://localhost:9000/v1/switches/switch_1/output \
  -H "Content-Type: application/json" \
  -d '{"channel": 2}'

Using Python

import requests

BASE_URL = "http://localhost:9000/v1"

# List switches
response = requests.get(f"{BASE_URL}/switches")
switches = response.json()['switches']
print(f"Available switches: {switches}")

# Get current channel
response = requests.get(f"{BASE_URL}/switches/switch_1/output")
current = response.json()['channel']
print(f"Current channel: {current}")

# Switch to channel 1 (for virtual switch with 4 channels, valid: 0-3)
response = requests.put(
    f"{BASE_URL}/switches/switch_1/output",
    json={"channel": 1}
)
if response.status_code == 200:
    print("Channel switched successfully")

Architecture

Components

  • __main__.py - Entry point, configuration loading, and daemon startup
  • listener.py - HTTP server and REST API endpoints (HTTPListener)
  • backend.py - Business logic and request handling (FOSBackend)
  • drivers/ - Hardware driver implementations
    • common.py - Base driver class and registry
    • weinert_eol.py - Weinert EOL switch drivers
    • agiltron_ffsw.py - Agiltron FFSW switch driver
    • virtual.py - Virtual switch for testing

Driver Development

To add support for a new switch model:

  1. Create a new driver file in drivers/
  2. Inherit from FOSDriver base class
  3. Implement required methods:
    • probe(device_id) - Check if driver handles the device ID
    • __init__(config) - Initialize the switch with config
    • get_info() - Return switch information
    • get_channel() - Get current channel (1-based)
    • set_channel(ch) - Set output channel (1-based)
  4. Register the driver in drivers/common.py

Example:

from .common import FOSDriver

class MySwitch(FOSDriver):
    @classmethod
    def probe(cls, device_id: str) -> bool:
        return device_id == "myvendor-mymodel"

    def __init__(self, config):
        # Initialize hardware connection
        pass

    def get_info(self) -> dict:
        return {"model": "MyModel", "outputs": 4}

    def get_channel(self) -> int:
        # Query hardware for current channel
        # Return 0-based or 1-based depending on your hardware
        return 1

    def set_channel(self, ch: int):
        # Validate channel range (example for 0-based with 4 channels)
        if ch < 0 or ch >= 4:
            raise ValueError(f"Channel {ch} out of range")
        # Send command to hardware
        pass

Troubleshooting

Serial Port Permissions

On Linux, you may need to add your user to the dialout group:

sudo usermod -a -G dialout $USER
# Log out and log back in for changes to take effect

Finding Serial Ports

# Linux
ls /dev/tty* | grep -i usb

# macOS
ls /dev/tty.* | grep -i usb

# List by ID (Linux)
ls /dev/serial/by-id/

License

This project is provided under the European Union Public License 1.2 (EUPL-1.2) license.