Skip to content

Repository files navigation

espgensam

espgensam is an unofficial ESPHome component and firmware that turns an ESP32 with an RS-485 transceiver into a standalone controller for Genelec SAM (Smart Active Monitor) speakers, communicating natively over the Genelec "GLM" bus and exposing monitor control directly to Home Assistant.


Features

  • Speaker Controls: Direct volume, mute, power/standby, input select, and telemetry reporting.
  • Group Presets: Switch between named calibrations (GLM's "groups") from Home Assistant, each with its own room EQ, level, delay, crossover and input routing per speaker. Read straight from your existing GLM 5 setup file. A Bypass Calibration switch turns the calibration off for a quick before-and-after comparison.
  • No GLM software or GLM network adapter required: Control monitors locally, using a cheap ESP32 microcontroller and RS-485 transceiver.
  • Native Home Assistant Integration: Control your Genelec SAM system from Home Assistant (optional)

screenshot of hub device in Home Assistant

screenshot of speaker device in Home Assistant

Use of this code is entirely at your own risk! I can't rule out that this could damage your Genelec or computer hardware, eat your tweeters, void your warranty, cause hearing damage, or, last but not least, ruin your audio fidelity. You have been warned.

You may also want to take a look at HLM, the Homebrew Loudspeaker Manager, which is a similar project to control Genelec SAM monitors from a (STM32/ESP32) microcontroller. It already implements most of the protocol's functionality. We have started collaborating to better understand the underlying GLM protocol.


Supported Hardware

Selecting a board is a one-line change to its packages: block in espgensam.yaml:

packages:
  # --- Board selection: exactly one of these ---
  board: !include packages/board_waveshare_esp32s3_rs485_can.yaml
  # board: !include packages/board_m5stack_atoms3_rs485_base.yaml
  # board: !include packages/board_m5stack_atoms3_rs485_iso.yaml

  gensam: !include packages/gensam.yaml

Everything else — monitors, entities, tunables — lives in packages/gensam.yaml and is shared.

Recommended boards:

  • MCU: ESP32-S3 dual-core
  • RS485 TX: GPIO17
  • RS485 RX: GPIO18
  • Direction Control: Explicit, GPIO21 (SP3485EN; /RE is tied to DE, so the receiver is disabled while transmitting)
  • Isolation: Galvanically isolated RS-485, with an onboard 120 Ω termination and 4.7 kΩ fail-safe bias
  • Status RGB LED: none — the onboard LEDs are power and bus-activity indicators. Bus state is reported through the Bus Status sensor in Home Assistant.
  • Rediscover button: none — use the Rediscover Monitors button entity.

Make sure the bus has exactly one terminator at the controller end. In practice: the board ships with its 120 Ω enabled through a jumper, and a GLM adapter carries one too — a passive resistor, so it terminates whether or not the adapter is powered. Adapter attached → pull the Waveshare jumper. Waveshare alone → leave it in.

NOT recommended:

2. M5Stack AtomS3 Lite + Atomic RS485 Base

  • MCU: ESP32-S3 dual-core
  • RS485 TX: GPIO6
  • RS485 RX: GPIO5
  • Direction Control: Automatic (Atomic RS-485 pulse-sensing circuit)
  • Isolation: NOT electrically isolated
  • Status RGB LED: GPIO35 (WS2812)
  • Rediscover button: GPIO41

While this hardware does work in practice, it has proven not to be ideal hardware for this use case due to the auto-direction circuit. It's also not electrically isolated, so more risky with e.g. ground loops through your Genelec or computer hardware.

This board needs a terminator somewhere on the bus. The Atomic RS485 Base has no termination resistor of its own (M5Stack's docs tell you to add one), and with nothing else attached it does not communicate at all — not degraded, dead. A GLM adapter's TERMINATOR port is enough, with the adapter otherwise idle. Running standalone, fit a 120 Ω across A/B.

3. M5Stack AtomS3 Lite + Isolated RS485 Unit (RS485-ISO)

  • MCU: ESP32-S3 dual-core
  • RS485 TX: GPIO2 — Grove Port A, yellow wire
  • RS485 RX: GPIO1 — Grove Port A, white wire
  • Direction Control: Automatic (CA-IS3082W with /RE tied to DE, DI grounded, and our TX line inverted onto that pin — the driver is on only while a 0 is on the wire, and a 1 is the line released to the bias)
  • Isolation: Galvanically isolated RS-485 with its own isolated DC-DC, rated 1000 VRMS; 4.7 kΩ fail-safe bias, no termination fitted
  • Status RGB LED: GPIO35 (WS2812)
  • Rediscover button: GPIO41

Not recommended. In three hours of normal listening this combination lost the entire monitor bus several times. It recovers unaided, but recovery re-runs speaker configuration, which deliberately silences output — so each event is a few seconds of audio dropout. An earlier session also enumerated a phantom monitor from a corrupted discovery reply. Entry 1 has never done either in weeks of use.

Note this is despite better frame-level integrity than entry 1 (0.38% CRC against 1.59%). Its failures are at enumeration level rather than frame level, and only the former is audible. See docs/rs485-transceiver-comparison.md §4.

  • Fit the 120 Ω that comes in the box, or terminate elsewhere. Nothing is fitted on the unit and there is no jumper for one.

RJ45 "GLM" Cable Pinout

Connect the RS-485 transceiver terminal block to a standard CAT5/6 RJ45 patch cable wired to T568B:

RJ45 Pin (T568B) Wire Color GLM Bus Signal RS485 Terminal
Pin 1 White / Orange Data A (D+) (non-inverting) A
Pin 2 Orange Data B (D-) (inverting) B
Pin 8 Brown GND (bus ground reference) GND
Pins 3–7 — Unconnected —

The GND row is board-dependent. The Waveshare's RS-485 terminal block has only A and B — there is no ground terminal, so pin 8 is simply left unconnected there, and the board works correctly that way. Its fail-safe bias network ties A to the isolated 3V3 rail and B to the isolated ground, so the isolated side already self-references to the bus through 4.7 kΩ and never actually floats. The Isolated RS485 Unit is the same in this respect: its four-way terminal is SHIELD / unconnected / A / B, the bus-side ground reaches the SHIELD pin only through 1 MΩ ∥ 1 nF, and it biases A/B against its isolated 5 V rail through the same 4.7 kΩ pair.


Configuration Reference

1. Declare Sub-Devices (esphome: devices:)

In espgensam.yaml, list the discrete monitor devices you want Home Assistant to create:

esphome:
  name: "espgensam"
  friendly_name: "Genelec SAM Controller"
  devices:
    - id: dev_subwoofer
      name: "Subwoofer"
    - id: dev_left_monitor
      name: "Left Monitor"
    - id: dev_right_monitor
      name: "Right Monitor"

2. Hub & Monitor Configuration (gensam:)

The pin keys belong in a board package; everything else is board-independent and lives in packages/gensam.yaml. They are shown together here for reference.

gensam:
  id: gensam_hub

  # --- Transceiver pins (board package) ---
  tx_pin: GPIO6
  rx_pin: GPIO5
  # de_pin: GPIO21      # Direction line, driven per frame. Omit on auto-direction modules.
  # re_pin: GPIO17      # Receiver enable, asserted once at boot.
  # power_pin: GPIO16   # 5 V DC-DC booster enable, for modules with their own rail.
  # se_pin: GPIO19      # Transceiver enable / shutdown, asserted once at boot.
  # tx_echoes_rx: false # Whether any of our own transmission can reach RX. Defaults to
  #                     # "no de_pin configured", which is right for every supported board. An
  #                     # auto-direction module's one-shot is triggered by our own start bit, so
  #                     # it is always late and the opening bits escape; a deliberately driven
  #                     # direction line is asserted first and leaks nothing. Set it only for a
  #                     # board that drives DE but leaves its receiver permanently enabled —
  #                     # getting it wrong the other way discards the start of every reply.
  # All pin keys accept `inverted: true`. `mode:` is ignored: the driver configures pull-ups
  # itself, and RMT rebinds tx_pin/rx_pin regardless.

  rx_buffer_size: 512

  # Coexistence with official GLM USB adapter
  yield_to_glm: true
  glm_inactivity_cooldown: 30s

  # Timing and filtering
  poll_interval: 1s                   # RS-485 physical keep-alive sampling
  telemetry_averaging_period: 60s     # In-memory averaging window for signal levels

  # Master volume mapping boundaries
  min_volume_db: -80.0                # Volume at slider = 0.0
  max_volume_db: 0.0                  # Volume at slider = 1.0
  startup_volume_db: -30.0            # Initial volume on boot

  # Diagnostic hub entities
  bus_status:
    name: "Bus Status"

  rediscover_button:
    name: "Rediscover Monitors"

  # Monitor bindings: associate physical speakers with Home Assistant devices
  monitors:
    - serial_number: "7350APM88123456"
      unique_id: 1842915
      name: "Subwoofer"
      device_id: dev_subwoofer
      # Optional. Fixes this speaker's routing at boot; omit it and the speaker keeps
      # whatever its own flash holds until a group is applied or you pick an option in
      # Home Assistant. One of: analog | aes3_a | aes3_b | aes3_sum
      input: aes3_sum

    - serial_number: "8330AP99234567"
      unique_id: 1654321
      name: "Left Monitor"
      device_id: dev_left_monitor

    - serial_number: "8330AP77345678"
      unique_id: 1987654
      name: "Right Monitor"
      device_id: dev_right_monitor

# Master group media player
media_player:
  - platform: gensam
    name: "Genelec SAM System"

Each monitor gets an Input select listing Analog, AES3 Channel A (Left), AES3 Channel B (Right) and AES3 Channel A+B (Sum). Routing is per speaker because that is how the hardware works: a GLM group can perfectly well run the subwoofer on AES3 while both main monitors are analog, so there is no single system-wide input to select.

Each monitor also gets three calibration controls: Bass Management Crossover (Full band for no bass management, or 50-120 Hz), Level (-60 to 0 dB, attenuation only) and Delay (0-192 ms). They are disabled by default, because a group preset normally owns them and overwrites them at its next push — enable them in Home Assistant for a speaker you want to trim by hand.

3. Group Presets (gensam: groups:)

A group preset is a named monitoring configuration, just like the Group buttons in GLM: which speakers take part, how each is fed, and the room calibration for each of them at one listening position. Switching between groups from Home Assistant re-sends the whole DSP block to every speaker.

Most setups should get their groups from their existing GLM calibration, with sam_file: below. Written out by hand instead, a group carries twenty EQ bands per speaker, so they live in their own file:

gensam:
  groups: !include gensam_groups.yaml

  # Applied once the speakers have been found, so they are never left in a state
  # you did not choose. Defaults to the first group; use `none` to apply nothing.
  default_group: "Main Listening Position"

  group_select:
    name: "Group Preset"
# gensam_groups.yaml
- name: "Main Listening Position"
  devices:
    - unique_id: 1842915          # matches a monitor's unique_id above
      source: aes3_sum            # analog | aes3_a | aes3_b | aes3_sum
      crossover: 90               # Hz, or full_band for a system without a subwoofer
      level_db: -1.9258           # per-speaker trim from AutoCal
      delay_samples: 289          # alignment delay, 48 kHz samples (max 9216 = 192 ms)
      lfe_channel: aes3_b         # subwoofers in surround setups only; default none
      lfe_level_db: -4            # whole dB, including the LFE +10 boost if set
      filters:                    # up to 20; the rest are left flat
        - {type: notch, frequency: 56.1739, gain: -6.05847, q: 4.68839}
        - {type: low_shelf, frequency: 118.711, gain: -0.177536}
        - {type: high_shelf, frequency: 14999, gain: -0.0199986}

type is notch (a peaking filter, as GLM labels it), low_shelf, high_shelf or bypass. Only notch takes a q. Set enabled: false on a device to mute it in that group rather than configure it.

lfe_channel is for a subwoofer in a surround setup, where the discrete ".1" channel reaches it on its own input alongside the bass-managed program. Leave it out for stereo and 2.1, which is what none means. When it is set, source must name a single channel rather than the A+B sum, so that the LFE feed is not also folded into the program path; importing handles this for you.

Filter order is the order the speaker's own filter slots run in, which differs by model: a two-way monitor takes two low shelves, two high shelves and then up to sixteen notches, while a subwoofer takes twenty notches and no shelves. Importing gets this right; if you write a group by hand, follow the same order.

Applying a group sets every speaker's Input select, Crossover, Level and Delay - and the LFE routing and level on a subwoofer that has them - so they always show what the speakers were last told. Changing one by hand takes effect immediately but does not alter the group, so the next group push - switching group, waking from standby, or a rediscovery - puts the group's own values back. While the two disagree, the hub's Group Modified diagnostic sensor is on.

Bypass Calibration works like GLM's "Cal bypassed" button, for comparing the system with and without its calibration. While it is on, every speaker plays with flat EQ, no level trim and no delay. Crossover, input routing and LFE stay as the group has them. Turning it off puts the calibration back. It stays in effect across group switches and standby.

  • Each speaker also has its own Bypass Calibration switch, disabled by default, for comparing one speaker at a time. A speaker is bypassed while either switch is on.
  • Both switches are off after every restart.
  • They only exist when groups are configured.
  • Because the level trim is removed as well, bypassed speakers can play louder than calibrated ones.

4. Importing an existing GLM setup (gensam: sam_file:)

Point the hub at a GLM 5 .sam setup file and every group in it becomes a group preset, so an existing AutoCal calibration does not have to be retyped:

gensam:
  monitors:
    - unique_id: 1842915
      # ...

  sam_file: "My Setup.sam"

  default_group: "Main Listening Position"
  group_select:
    name: "Group Preset"

The file is read while ESPHome validates the configuration, which happens on every esphome config, compile and run. Re-run calibration, save in GLM, rebuild: there is nothing to regenerate and no converted file to keep in step. esphome config prints the groups in full, which is how you see what was imported.

Presets appear in the order the setup file lists them, before any you also wrote in groups:. That order is what the Group Preset select offers and what default_group falls back to, so a hand-written extra — a mute-all, a late-night trim — lands after the calibrated positions.

Only devices named in your monitors: block are configured. The rest are skipped with a warning: a GLM setup file can retain a speaker that is no longer connected, or one that was never really there. With no monitors: at all, every device in the file is taken as yours.

Anything the import cannot carry across is reported rather than dropped quietly. Pay attention to those warnings: they are the difference between the group sounding as GLM calibrated it, and sounding off.

Where the file lives

The path is relative to the directory holding your ESPHome YAML, but ~ is expanded and an absolute path is taken as given, so it can point straight at GLM's own setup directory:

  sam_file: "glm/My Setup.sam"
  sam_file: "~/Documents/Genelec/GLM5/Setup Files/My Setup.sam"

Building from the ESPHome dashboard or the Home Assistant add-on needs the .sam copied into the configuration directory instead. GLM setup names usually contain spaces, so quote them.

If you would rather have the YAML

tools/sam2yaml.py runs the same conversion and writes the groups file out, which is the way to inspect it, diff two GLM exports, or correct a file the component will not accept:

python3 tools/sam2yaml.py "My Setup.sam" \
    --monitors 1842915,1654321,1987654 \
    -o gensam_groups.yaml

--monitors does by hand what monitors: does automatically. Include the result with groups: !include gensam_groups.yaml and leave sam_file: out. The tool needs PyYAML; if your system Python lacks it, pip install pyyaml, or run it with ESPHome's own interpreter.


Getting Started

1. Configure Secrets

cd espgensam
cp secrets.yaml.example secrets.yaml

Edit secrets.yaml with your Wi-Fi credentials and ESPHome API key.

2. Build & Flash

Select your board in the packages: block of espgensam.yaml (see Supported Hardware), then:

esphome run espgensam.yaml

Monitor serial numbers and entities live in packages/gensam.yaml, so they carry over unchanged if you swap boards.


License

This project is licensed under the GNU General Public License v3.0.

About

Unofficial ESPHome component for managing Genelec SAM monitors

Resources

Stars

2 stars

Watchers

0 watching

Forks

Used by

Contributors

Languages