Logic Encoder Blog

Fixing SatPI on the Vu+ Duo 4K SE: FBC tuners, DiSEqC and DVBViewer

Sep 14, 2026 Logic Encoder 20 min read

Running SatPI on a Vu+ Duo 4K SE sounds like it should be straightforward.

The receiver already has FBC tuners. SatPI can expose DVB tuners over the network using the SAT>IP protocol. A client such as DVBViewer can discover the receiver, tune a satellite transponder and receive an MPEG transport stream over the network.

In practice, the Vu+ Duo 4K SE turned out to be a very different target from a conventional Linux DVB system.

I spent a considerable amount of time debugging the actual receiver rather than trying random configuration changes. The important part of this investigation was that the failures were not all caused by SatPI itself. Some came from the Broadcom bcm7335 / NEXUS driver stack, some from assumptions in the original SatPI DVB implementation, and one particularly annoying problem came from tuning parameters supplied by DVBViewer.

The final result is a working SatPI installation on a Vu+ Duo 4K SE running OpenPLi 9.2.

DVBViewer discovers the SAT>IP server, DiSEqC switching works, the correct FBC tuner is selected, DVB-S2 transponders lock, real RTP/TS data reaches the client, and Enigma2 can continue using the other tuner bank.

This article explains what actually went wrong and how I verified the fixes.

Vu+ Duo 4K SE — dual FBC tuner banks, one receiver

The short version

If you are looking for the answer before reading the complete investigation, this is the important part:

The Vu+ Duo 4K SE should not be treated as 16 completely independent DVB frontends.

The receiver exposes 16 Linux DVB frontends, but they represent two FBC banks with root and child relationships.

On my installation:

  • Slot A belongs to Enigma2.
  • Slot B is used by SatPI.
  • frontend9 is the working SatPI root tuner.
  • Its FBC children cannot reliably perform DiSEqC themselves.
  • DiSEqC therefore has to be routed through the root frontend.
  • The NEXUS demux requires its own specific handling of DMX_SET_SOURCE, PID filters and buffer configuration.
  • pids=all cannot be implemented by sending PID 8192 to this driver because that can crash the kernel.
  • DVBViewer can send stale DVB-S2 tuning hints which prevent the NEXUS demodulator from locking.
  • SatPI therefore needs to retry a failed tune with pilot/FEC/rolloff hints relaxed to AUTO.
  • SSDP discovery also needs the correct Ethernet interface because advertising 0.0.0.0 makes the server undiscoverable even when streaming itself works.

Once those details were addressed, I could verify actual TS packets rather than merely seeing a Sat>IP server in DVBViewer.

Why the original SatPI installation failed

The first useful clue was upstream SatPI issue #210, which reports that on the Vu+ Duo 4K SE only the first eight tuners work.

That description was close to what I was seeing, but it did not explain the whole problem.

The receiver actually exposes:

/dev/dvb/adapter0/frontend0
/dev/dvb/adapter0/frontend1
...
/dev/dvb/adapter0/frontend15

That gives sixteen DVB frontends.

It is tempting to assume that this means sixteen independent tuners.

It doesn’t.

The Vu+ Duo 4K SE uses two FBC tuner banks, and the Linux/NEXUS implementation exposes the relationship between root frontends and their FBC children in a way that matters when you try to control the hardware directly.

The measured topology on my receiver was:

SatPI frontendLinux frontendPhysical connection
fe=1frontend0Tuner A, motor-controlled dish
fe=2frontend1Tuner A, 4-port DiSEqC switch
fe=3-8frontend2-7FBC children of A
fe=9frontend8Tuner B, direct 23.5E input
fe=10frontend9Tuner B, DiSEqC switch
fe=11-16frontend10-15FBC children of frontend9

Two FBC tuner banks — which frontend is owned by Enigma2, which belongs to SatPI, and which are dead or DiSEqC-incapable

The important discovery was that the physical connections were not equivalent.

frontend9 was connected to the DiSEqC switch and could reach the required satellite positions. The child frontends below it could not perform the DiSEqC operation themselves.

That distinction became the foundation of the fix.

First: prove which tuner actually works

One of the easiest mistakes with FBC hardware is to enable every frontend because the operating system reports that they exist.

That makes the problem worse.

If SatPI advertises several frontends and the client chooses between them, DVBViewer can end up on a tuner that has:

  • no usable cable,
  • no DiSEqC capability,
  • or the wrong FBC relationship.

The result looks like a random tuning failure.

In my case, the measured results were much clearer.

The working tuner bank produced real transport packets for multiple satellite positions:

  • 23.5E: 2044 packets
  • 19.2E: 6139 packets
  • 13E: 4095 packets

The working SatPI configuration therefore exposes the known-good FBC root rather than pretending that all 16 frontends are equally usable.

The final split is:

Enigma2
  └── Slot A
      ├── frontend0
      ├── frontend1
      └── frontend2-7

SatPI
  └── Slot B
      ├── frontend8
      └── frontend9-15

But SatPI does not expose every one of those Slot B frontends to clients.

The working configuration enables the DiSEqC-capable root frontend and disables the unusable paths.

That is an important practical lesson:

With FBC hardware, more advertised tuners does not necessarily mean more usable SAT>IP tuners.

The first real SatPI problem: DiSEqC on FBC children

The original DiSEqC implementation assumed that the selected frontend file descriptor could simply be used to send the DiSEqC command.

That does not work correctly with the bcm7335/NEXUS implementation.

I could reproduce the failure directly: an FBC child frontend could tune in situations where the root frontend was appropriate, but attempting to send DiSEqC through the child path failed.

The fix was to use the FBC root connection.

Conceptually:

DVBViewer
    |
    v
SatPI child frontend
    |
    | FBC relationship
    v
root frontend
    |
    v
bcm7335 / NEXUS
    |
    v
DiSEqC switch

The child duplicates the already-open root frontend file descriptor and sends the DiSEqC sequence through that root.

The actual sequence was also important:

tone OFF
target voltage
master command
mini-burst
tone

The implementation was adjusted to match the minisatip behaviour rather than treating the FBC child as an independent DiSEqC-capable frontend.

The timing also mattered. The working implementation uses the timing behaviour expected by the driver/minisatip model rather than the original values.

The result was measurable: the root frontend could reach the required switch positions and deliver real TS packets from 23.5E, 13E and 19.2E.

DiSEqC chain — one coax from the receiver through a 4-port switch to three orbital positions

The two-digit frontend bug

There was another smaller problem that was surprisingly easy to overlook.

The original path manipulation used a replacement operation that worked for a one-digit frontend number but broke when the frontend number became two digits.

In other words, logic that was effectively safe for:

frontend8

was not safe for:

frontend10
frontend11
...
frontend15

The fix was to replace the complete frontendN suffix rather than assuming a single character represented the frontend number.

This sounds trivial, but on a receiver with sixteen frontends it is exactly the kind of assumption that survives unnoticed until FBC children are actually tested.

The NEXUS demux is not a normal Linux DVB demux

This was one of the most important findings during the investigation.

The bcm7335/NEXUS driver does not behave like the generic DVB demux model that SatPI originally assumed.

I verified this using an ioctl tracer and standalone C tests rather than relying only on SatPI logs.

DMX_SET_SOURCE must use the demux’s own file descriptor

The source has to be configured on the same demux file descriptor that owns the filters.

For example:

/dev/dvb/adapter0/demux8

uses source value:

8

It is not an additional offset such as +32 or +40.

That distinction is important because an implementation can appear logically correct while silently selecting the wrong NEXUS source.

PID filters behave differently too

Another unexpected behaviour was that simply installing a PES filter did not produce transport packets.

A filter setup such as:

SET_PES_FILTER

alone resulted in zero useful packets.

The driver needed valid DMX_ADD_PID operations to start the demux path.

SatPI normally gets those initial PIDs naturally because clients request common PSI/SI data such as:

0
1
16
17
18

Once the demux is actually running, additional PID filters can be added while the stream is active.

That was also tested.

A new DMX_ADD_PID becomes active after a delay of several seconds, which is important when testing the system. A five-second capture can easily give the impression that a newly requested service did not work.

On this hardware I found roughly a 32-channel limit per demux file descriptor. Bursts that stayed around 10, 16 or 24 PID channels worked, while a larger burst could fail.

That limit directly influenced how pids=all had to be implemented.

Why pids=all cannot simply mean PID 8192

Many SAT>IP implementations use:

pids=all

as a convenient way to request the complete transport stream.

The obvious implementation would be to pass the special 0x2000 / 8192 value to the driver.

On this NEXUS driver, that is unsafe.

Testing it caused a kernel oops inside:

NEXUS_Recpump_AddPidChannel_impl

So the final SatPI implementation does not pass the literal 8192 PID to the kernel.

Instead, pids=all is emulated in userspace.

The process is roughly:

PAT
 |
 +-- discover PMT PIDs
 |
 +-- read PMTs
      |
      +-- video
      +-- audio
      +-- PCR
      +-- ECM
      +-- other required PIDs

The implementation caps the automatically selected PID set so that it remains below the practical NEXUS demux limit.

pids=all must be emulated in userspace — walking PAT and PMTs instead of sending raw PID 8192 to the kernel

This was not just a theoretical implementation.

A real pids=all test produced:

  • 201,527 TS packets
  • 26 unique PIDs
  • real video PIDs including 566 and 562
  • no kernel crash

After reboot, another long-running test produced:

  • 235,886 TS packets
  • 23 active PIDs
  • approximately 8–12 Mbps
  • real PMT/ES traffic rather than only SI packets

That distinction matters because seeing PAT/SDT packets is not proof that the complete service stream is working.

A useful discovery: some of my own tests were wrong

During the investigation I also found several traps in my test code.

This is worth documenting because someone debugging the same hardware can otherwise waste hours blaming SatPI.

SEC_TONE_ON is zero

I initially treated the enum as if SEC_TONE_ON had value 1.

It doesn’t.

On this API:

SEC_TONE_ON = 0

A test program using the wrong value can therefore make DiSEqC debugging extremely confusing.

DMX_ADD_PID expects an int

Another test initially passed an unsigned short.

The driver reads four bytes, so the result was garbage PID data, including values such as:

2621441

The correct interface uses an int.

DVB-S2 needs the complete property set

The tuner also needs a proper DVB-S2 property set.

The successful tests included:

DELIVERY_SYSTEM = SYS_DVBS2
MODULATION      = 8PSK / QPSK
FEC             = AUTO
INVERSION       = AUTO
ROLLOFF         = AUTO
PILOT           = AUTO

Without the complete set, the receiver could report misleading status values such as 0x07 while delivering no useful TS.

For this receiver:

0x07 = no lock
0x1f = full lock

That became one of the basic measurements used throughout the debugging process.

The most frustrating problem: DVBViewer found the server but still played nothing

This was the point where the system became especially deceptive.

Eventually DVBViewer could discover the SatPI server.

The server was visible.

The client could start a session.

But the channel still would not play.

The tuner remained at:

status = 0x07

and there was no useful transport stream.

At first this looked like another DiSEqC or demux problem.

It wasn’t.

I captured the exact DVBViewer SETUP request:

SETUP ?src=1&freq=11856&msys=dvbs2&plts=off&fec=34&pol=v&ro=0.20&sr=29900&mtype=8psk

The important part was:

plts=off
fec=34

The actual transponder used in the test has pilots enabled.

DVBViewer was forwarding the values stored in its channel list, including stale tuning hints.

A parameter bisect revealed the important behaviour:

plts=off + explicit FEC

does not lock on this bcm7335/NEXUS demodulator.

Each parameter by itself was acceptable.

The combination was not.

That means a SAT>IP server cannot always blindly trust every DVB-S2 hint supplied by a client.

The practical fix: retry with AUTO tuning hints

The solution was not to modify the DVBViewer channel list and hope every future client has correct parameters.

The more robust solution is inside SatPI.

When an explicit tuning request fails to lock, the patched Frontend::setupAndTune() checks whether the request contains explicit pilot, FEC or rolloff hints.

If the first lock attempt times out, SatPI retries with those hints relaxed to AUTO.

Conceptually:

Client request
     |
     v
Tune using supplied parameters
     |
     +---- LOCK ----> stream
     |
     +---- timeout
              |
              v
       relax pilot/FEC/rolloff
              |
              v
          retune
              |
              v
            LOCK

This is much more useful than requiring the user to repair every stale channel entry.

It also makes the SAT>IP server more tolerant of different clients and old channel databases.

The exact DVBViewer request above was tested after the change and reached:

0x1f

with real RTP/TS data.

There was another DVBViewer problem: the wrong tuner could be selected

Even after fixing the tuning hints, DVBViewer could still land on the wrong frontend if too many SatPI streams were advertised.

The problematic pool included:

stream9  -> frontend8
stream11-16 -> FBC children

frontend8 was a dead cable path in this particular installation.

The FBC children could not perform the required DiSEqC operation.

So DVBViewer could appear to work randomly depending on which tuner SatPI assigned to the session.

The solution was deliberately restrictive.

Only the known-good DiSEqC-capable root stream is advertised for normal DVBViewer use.

In the final configuration:

stream10 -> frontend9

is the working physical input.

The unused/dead paths are disabled.

This makes the system less theoretically flexible but much more reliable in real use.

SSDP discovery was a separate problem

Another interesting failure happened after the actual streaming path was already working.

DVBViewer could stream from SatPI if given the server manually, but its automatic SAT>IP discovery found nothing.

The reason was visible in the SSDP response.

SatPI was advertising:

LOCATION: http://0.0.0.0:8875/desc.xml

That is obviously not a usable address for another computer.

The server was replying to M-SEARCH correctly, but the advertised location was wrong.

The receiver was also running a VPN interface, which made automatic interface selection unreliable.

The fix was simple and explicit:

--iface-name eth0

The SSDP response then advertised:

LOCATION: http://<receiver-lan-ip>:8875/desc.xml

DVBViewer discovery immediately became useful.

This is a good example of why a SAT>IP problem can look like a protocol failure when the actual problem is just the advertised network address.

SIGPIPE and RTSP stability

There were also two stability issues that had to be fixed before long-running testing became useful.

The first was SIGPIPE.

If a SAT>IP client closed its TCP connection while SatPI was still writing stream data, the process could receive:

Broken pipe

and terminate.

The fix was to ignore SIGPIPE in main.cpp:

signal(SIGPIPE, SIG_IGN);

The second problem involved an RTSP control session being idle for too long.

The SAT>IP feed could continue correctly, but the control session eventually expired.

The test wrapper therefore sends an RTSP OPTIONS keepalive every 30 seconds.

With that in place, an RTSP feed was verified for approximately 52 minutes, with:

  • 122k RTP packets
  • 669k TS packets
  • clean TEARDOWN

That gave me considerably more confidence than a short successful test.

How I verified the final system

I deliberately did not consider the SatPI work finished simply because:

SatPI was running

or:

DVBViewer discovered the server

Those checks are too weak.

The final verification used several layers.

1. Tuner lock

The frontend had to reach:

0x1f

rather than merely report a session.

2. Real TS packets

The stream had to contain real transport packets, not only:

0x1FFF

NULL packets.

3. Real service PIDs

The stream had to contain PAT/PMT and actual video/audio PIDs.

4. Multiple satellite positions

The working DiSEqC path was tested on:

  • 23.5E
  • 13E
  • 19.2E

5. DVBViewer

The actual DVBViewer request was replayed and verified rather than using a simplified custom request only.

6. Reboot

The receiver was rebooted and the entire configuration was checked again.

After reboot:

  • SatPI autostarted,
  • port 554 was available,
  • port 8875 was available,
  • Enigma2 was still running,
  • the tuner split was preserved,
  • pids=all still produced real transport data.

That last test is particularly important because a configuration that works only until the next reboot is not a finished deployment.

Final SatPI tuner layout

The final setup intentionally separates Enigma2 and SatPI.

Vu+ Duo 4K SE
|
+-- FBC Slot A
|   |-- frontend0
|   |-- frontend1
|   +-- frontend2-7
|
|   +-- Enigma2
|
+-- FBC Slot B
    |-- frontend8
    |-- frontend9  <- SatPI root / DiSEqC switch
    +-- frontend10-15

Enigma2 keeps its own tuner bank.

SatPI owns the working root frontend on the other bank.

No frontend is opened by both applications.

This makes the arrangement predictable and avoids two different applications trying to control the same DVB frontend simultaneously.

One receiver — SAT>IP stream to the network and Enigma2 live TV on its own tuner bank, no tuner contention

The working test request

For a direct HTTP test on the receiver, the working form is:

curl -s --max-time 60 \
  'http://<receiver-ip>:8875/?freq=11739&pol=v&sr=29900&msys=dvbs2&fe=9&pids=all' \
  -o /tmp/sp.ts

One very important detail:

pol=v

must be lowercase.

During testing I found that:

pol=V

is not interpreted the same way by the SatPI parser.

The uppercase value fell through to the default behaviour, resulting in the wrong voltage being selected for the vertical transponder.

The symptoms were extremely misleading:

  • HTTP returned 200 OK
  • SatPI appeared to accept the request
  • the client received data
  • but the transport stream consisted of NULL packets

So if you ever see a perfectly healthy-looking SAT>IP HTTP/RTP session containing only NULL packets, check the polarity parameter before assuming the network or RTP layer is broken.

What this means for other Vu+ Duo 4K SE owners

The biggest lesson from this project is that the Vu+ Duo 4K SE is not simply a normal Linux DVB machine with sixteen interchangeable /dev/dvb frontends.

The FBC architecture and the bcm7335/NEXUS driver matter.

If you are troubleshooting SatPI on this receiver, I would check the following in this order:

1. Identify the physical frontend

Do not start by enabling everything.

Find which physical LNB input actually receives the satellite signal you need.

2. Test DiSEqC independently

Verify the root frontend can switch between the required satellite positions.

3. Do not send DiSEqC through an FBC child

Route the operation through the root frontend.

4. Verify the NEXUS demux

Make sure DMX_SET_SOURCE is applied to the correct demux fd and uses the frontend index expected by the driver.

5. Watch PID limits

Do not blindly create thousands of kernel PID filters.

6. Never send literal PID 8192 for pids=all

On this driver that can cause a kernel oops.

7. Check polarity case

Use:

pol=v

not:

pol=V

8. Be suspicious of stale DVB-S2 hints

Especially:

plts
fec
ro

If the tuner never reaches 0x1f, retrying with AUTO values can be the difference between a dead stream and a working one.

9. Advertise only usable SAT>IP tuners

A client choosing randomly among broken FBC children is not useful.

10. Test after reboot

The final test should include actual TS packets after a clean boot.

Current result

The patched SatPI installation now provides the behaviour I originally expected from the Vu+ Duo 4K SE:

  • DVBViewer discovers the SAT>IP server.
  • The correct FBC root frontend is selected.
  • DiSEqC switches between the required satellite positions.
  • DVB-S2 transponders reach full lock.
  • Real MPEG-TS data reaches the client.
  • pids=all works without sending the dangerous 8192 PID to the kernel.
  • Enigma2 continues running on its own tuner bank.
  • SatPI survives reboot and starts automatically.
  • The installation works with the bcm7335/NEXUS driver stack used by this receiver.

The fork containing the changes, together with the SatPI configuration and deployment work, is available here:

github.com/logicencoder/satpi

The important point is not that a patched binary happens to work on one receiver.

The useful result is understanding why the original implementation failed.

The failures came from several different layers:

DVBViewer
   |
   | SAT>IP / RTSP
   v
SatPI
   |
   |-- stale DVB-S2 hints
   |-- FBC child/root handling
   |-- DiSEqC routing
   |-- PID management
   +-- RTSP/SSDP stability
   |
   v
Linux DVB API
   |
   v
bcm7335 / NEXUS
   |
   |-- unusual demux behaviour
   |-- FBC relationships
   +-- DiSEqC limitations
   |
   v
LNB / DiSEqC switch / satellite

Every layer had its own bug — client hints, daemon crashes, FBC fd handling, driver limits, physical wiring

Once each layer was tested independently, the problem stopped looking like one mysterious “SatPI doesn’t work on Vu+” bug.

It became a set of concrete, reproducible hardware and software behaviours — each with a measurable fix.

Upstream SatPI vs the Logic Encoder fork — same hardware, different driver handling

Frequently asked questions

Does SatPI work on the Vu+ Duo 4K SE?

Yes, with the required changes for the bcm7335/NEXUS FBC implementation.

The important part is not to expose every Linux frontend as an independent SAT>IP tuner. The working configuration uses the correct FBC root and routes DiSEqC through it.

Why does DVBViewer discover SatPI but show no picture?

There are several possible causes.

The two most important ones found during this investigation were:

DVBViewer selected an unusable FBC/dead frontend.

DVBViewer supplied stale DVB-S2 hints such as plts=off together with an explicit FEC.

The patched SatPI configuration restricts the tuner pool and retries failed tunes with pilot/FEC/rolloff set to AUTO.

Why does SatPI return data but DVBViewer still shows no signal?

Check whether the data is real TS data.

An HTTP 200 OK does not prove that the tuner locked.

Also check the polarity parameter. On this setup:

pol=v

works for a vertical transponder.

Using:

pol=V

can result in the wrong default polarity and an all-NULL stream.

Can the FBC child frontends perform DiSEqC?

Not reliably on the bcm7335/NEXUS implementation tested here.

The DiSEqC command needs to go through the FBC root frontend.

Can pids=all be passed directly to the NEXUS driver?

No.

Sending PID 8192 caused a kernel oops during testing.

The safe implementation emulates pids=all in userspace by discovering the relevant PSI/PMT/ES/PCR/ECM PIDs and adding them individually.

Can Enigma2 and SatPI run at the same time?

Yes.

The working configuration splits the tuner banks so that Enigma2 owns one side and SatPI owns the other.

The important rule is that the same frontend should not be opened by both applications.

Does it survive reboot?

Yes.

The final installation was reboot-tested. SatPI autostarts, the tuner split remains intact, and real pids=all transport data was verified after boot.

Does this require OpenATV or VTi?

No.

The tested installation is OpenPLi 9.2. The patched SatPI binary is statically linked so it does not depend on the newer host glibc/libstdc++ versions used by the build toolchain.

What should I check first if I reproduce the problem?

Start with these three things:

  1. Is the correct physical FBC root selected?
  2. Does the frontend reach 0x1f?
  3. Is DVBViewer sending stale plts/fec/rolloff values?

If those three are correct, move down into the NEXUS demux and PID handling.

Final takeaway

The original problem looked like “SatPI only uses eight tuners on the Vu+ Duo 4K SE”.

After actually tracing the DVB calls and testing the hardware, that description turned out to be incomplete.

The real problem was the interaction between FBC root/child frontends, DiSEqC, the bcm7335/NEXUS demux, SAT>IP PID handling and DVB-S2 tuning hints coming from the client.

That is why simply changing SatPI.xml was never going to be enough.

The useful fix came from testing each layer against real hardware and keeping the measurements — lock status, packet counts, PIDs, DiSEqC behaviour and reboot persistence — rather than treating “the process is running” as proof that SAT>IP works.

And that is also why this setup is now much easier to troubleshoot: when something fails, there is a concrete measurement to look at instead of another round of blind configuration changes.

Share this post

Previous ETH Gas Live Tracker — Check Ethereum Gas Fees Before Every Transaction Next What Wash Trading Looks Like in Raw Trade Data — 55 Million MEXC Trades Analyzed

Leave a comment

Your email address will not be published. Required fields are marked *