Edge-O-Matic 3000 and funscripts

I have a an edge-o-matic that just shipped. For those who don’t know: Edge-o-Matic 3000 – Maus-Tec
I see that it has websocket connectivity, and I know people were talking about integrating this with an OSR2 years ago. Id like to know if anything happened with that. Or if anyone here knows of a way to integrate it with funscripts. I’m not really sure what the thing is capable of with integration because I don’t have it yet. What I’d like to be able to do is to play a funscript either on this, or another device, but if I get too close the edge-o-matic turns down the device.

Thanks in advance!

1 Like

It’s an easy to make plugin for MFP and I’ve made one for someone as a proof of concept but as I dont have EOM its hard for me to test it. The plugin reads the arousal value from EOM and then sets a speed limit on stroke axis based on that. But it was all guess work so it definitely needs some tweaking.

As a side note, someone was smoking some good shit when figuring out the price for EOM. I would maybe buy it if it was like 20% of the current price.

2 Likes

yeah when i bought it I thought that the DIY route was impossible, like he took down all the plans for it, but I just found out that maus-tec was not the original creator and the plans are still out there. However sourcing the pressure sensor part is not easy.

But I’ll do some testing for you, if you’d like. I have been reading the manual and the thresholds are all tweak-able on the board itself, idk if that can be translated to MFP or vice versa.

Save as EoM3000.cs into plugins folder.
You can configure/start it via the plugins popup at the top of the window. You have to set the ip of your eom.
secondsPerStroke variable calculation is what needs to be tweaked.

//#r "name:System.Net.Sockets"
//#r "name:System.Net.WebSockets.Client"

using MultiFunPlayer.Plugin;
using MultiFunPlayer.Common;
using System;
using System.IO;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Reflection;
using System.Linq;
using Newtonsoft.Json.Linq;

namespace Plugin;

public class EoM3000PluginSettings : PluginSettingsBase
{
    private Uri _uri = new Uri("ws://127.0.0.1:80/");

    public Uri Uri
    {
        get => _uri;
        set => SetAndNotify(ref _uri, value);
    }

    public override UIElement CreateView() => CreateViewFromString(
        """
        <UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                     xmlns:converters="clr-namespace:MultiFunPlayer.UI.Converters;assembly=MultiFunPlayer"
                     xmlns:material="http://materialdesigninxaml.net/winfx/xaml/themes"
                     mc:Ignorable="d"
                     d:DesignHeight="450" d:DesignWidth="800">
            <UserControl.Resources>
                <converters:UriToStringConverter x:Key="UriToStringConverter"/>
            </UserControl.Resources>
            <StackPanel>
                <TextBox Text="{Binding Uri, Converter={StaticResource UriToStringConverter}}"
                         material:HintAssist.Hint="websocket uri"
                         Style="{StaticResource MaterialDesignFloatingHintTextBox}"
                         Width="250"
                         VerticalAlignment="Center"/>
            </StackPanel>
        </UserControl>
        """
    );

    public override void HandleSettings(JObject settings, SettingsAction action)
    {
        if (action == SettingsAction.Saving)
            settings[nameof(Uri)] = Uri?.ToString();
        else if (action == SettingsAction.Loading)
            if (settings.TryGetValue<Uri>(nameof(Uri), out var uri))
                Uri = uri;
    }
}

public class EoM3000Plugin : AsyncPluginBase
{
    public EoM3000PluginSettings Settings { get; }

    public EoM3000Plugin(EoM3000PluginSettings settings) => Settings = settings;

    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        using var client = new ClientWebSocket();

        Logger.Info($"Connecting to: {Settings.Uri}");
        await client.ConnectAsync(Settings.Uri, cancellationToken);

        while (!cancellationToken.IsCancellationRequested)
        {
            await Task.Delay(100, cancellationToken);

            var o = JObject.Parse(Encoding.UTF8.GetString(await client.ReceiveAsync(cancellationToken)));
            if (!o.TryGetObject(out var readings, "readings")
             || !readings.TryGetValue<double>("arousal", out var arousal))
                continue;

            arousal = Math.Clamp(arousal, 0, 600);
            var factor = Math.Pow(arousal < 200 ? 0 : MathUtils.Clamp01((arousal - 200) / 400), 4);
            var secondsPerStroke = MathUtils.Lerp(0.001, 10, factor);

            InvokeAction<DeviceAxis, double>("Axis::SpeedLimitSecondsPerStroke::Set", DeviceAxis.Parse("L0"), secondsPerStroke);
            Logger.Info("Arousal: {0}, SecondsPerStroke: {1}", arousal, secondsPerStroke);
        }
    }
}

this very well could be because I’m fucking around without actually having the device yet, but when I load the plugin it returns a compile error : EoM3000.cs(89,13): error CS4008: Cannot await ‘void’

Yea I edited the post, forgot that i was on develop build that has some changes.

sweet thanks, seems to be working now. I might get the delivery by the end of the week. so I may have some input by then.

Wow- was literally just thinking about this when this thread caught my eye. I do have an EoM, and just gave this a shot.

I’ve been able to get the plugin running, and it seems as though it connects to the EoM… but I’m not seeing any real change in stroking… not sure if I’m doing anything wrong. I’m using a Nimblestroker driven by a t-code connectivity module.

You also have to enable speed limit on L0, the plugin does not enable it.

Got my device today, currently messing around with, but I’m not getting how to use your plugin. I have it running and its connected (I think; the plugin shows it as running with no errors and the correct URL for the EOM) Im not sure If the stimulation device should be connected to the EOM or to MFP. Or does this only work with a stroker device? I tried both ways, but it seems that the script plays as normal even when pressure hits the threshold, and when connected to the EOM the script is not playing, it just increases motor speed.

The plugin reads arousal level from eom and sets speed limit on L0, it does not control the eom. You need to enable speed limit on L0 in MFP. The plugin sets the speed limit value.
To see if it correctly connects to the eom you can check the log files, they should show arousal readings.

Thanks for clearing that up. Was this per chance made with old firmware? I heard that EOM .6 firmware worked with xtoys but the newest version does not work reliably. I did all the things you instructed but I don’t see any data from the EOM in the (debug level) log.

EDIT That seems to be one of the issues. I downgraded to .6 and i got data for a few seconds, but now nothing again…

EDIT 2 the reason it stopped working was because it updated itself to 1.1.1 automatically.

I’m hoping to build a protogasm soon - do you think this code will work similarly for that device will I have to make adjustments? Either way thank you for providing this code

@senorgif2 i didnt see any changes in api on github so i dont know

@se2131 no idea

yeah that was 100 percent the issue, i rolled it back to 0.6.4 and MFP is able to pick up the data.

In my testing however, I find the plugin very unreliable. Idk if tweaking the math will help that, but it seems like the speed limit is not being applied when Arousal increases (in this case I’m just squeezing by hand. Going by the logs it seems that its not picking up arousal changes consistently, this is probably an issue with the device. I also notice that the EOM screen freezes up a lot when showing the arousal chart, while connected to the plugin.

I have no idea how one could do this, but it might be more reliable to make a plugin that can accept data via serial input. The EOM can output data through serial just like the older nogasm system does. The manual mentions that its backwards compatible with nogasm monitoring applications, but I cant seem to find anything of the sort.

EDIT I found one application: xtoys. It seems to work ok, except that it reads arousal in a different way.

Is there anyway to integrate this with the handy? Infinite edging in VR would be insane

1 Like

technically the plugin already passes data between a 0.6.4 firmware EOM3000 and any device that can connect to MFP (including a handy)

The nogasm toy on xtoys.app is also able to receive serial data (USB) from the EOM, once you enable legacy serial mode, and you can connect a handy to that as well.

The issue with both of these is that neither one works all that well, at least in preliminary testing. xtoys it at least consistent, but it reads arousal incorrectly. the EOM seems to have connection issues with the plugin above.

So I figured “what the hell i might as well see if AI can adapt the plugin for serial input” And to my absolute surprise claude.ai was able to do it, after a few rounds of errors and tweaking. data was coming in reliably and triggering the speed brake as you would expect. I don’t know a thing about coding c#, the best i know is a few high level general coding concepts and how to read error messages. I am so amazed that this actually worked. I’m sure people who actually know what they are doing will tell me that it was an easy adaptation to make.

I have this in my plugins folder as EOM3000serial.cs

//#r "name:System.Net.Sockets"
//#r "name:System.Net.WebSockets.Client"

using MultiFunPlayer.Plugin;
using MultiFunPlayer.Common;
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Reflection;
using System.Linq;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;

namespace Plugin
{
    public class EoM3000PluginSettings : PluginSettingsBase
    {
        private string _portName = "COM3";
        private int _baudRate = 115200;

        public string PortName
        {
            get => _portName;
            set => SetAndNotify(ref _portName, value);
        }

        public int BaudRate
        {
            get => _baudRate;
            set => SetAndNotify(ref _baudRate, value);
        }

        public override UIElement CreateView()
        {
            return CreateViewFromString(@"
<UserControl xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
             xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
             xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006""
             xmlns:d=""http://schemas.microsoft.com/expression/blend/2008""
             mc:Ignorable=""d""
             d:DesignHeight=""450"" d:DesignWidth=""800"">
    <StackPanel>
        <TextBox Text=""{Binding PortName}""
                 Style=""{StaticResource MaterialDesignFloatingHintTextBox}""
                 Width=""250"" VerticalAlignment=""Center"" />
        <TextBox Text=""{Binding BaudRate, StringFormat='{}{0:0}'}"" 
                 Style=""{StaticResource MaterialDesignFloatingHintTextBox}""
                 Width=""250"" VerticalAlignment=""Center"" />
    </StackPanel>
</UserControl>");
        }

        public override void HandleSettings(JObject settings, SettingsAction action)
        {
            if (action == SettingsAction.Saving)
            {
                settings[nameof(PortName)] = PortName;
                settings[nameof(BaudRate)] = BaudRate;
            }
            else if (action == SettingsAction.Loading)
            {
                if (settings.TryGetValue<string>(nameof(PortName), out var portName))
                    PortName = portName;
                if (settings.TryGetValue<int>(nameof(BaudRate), out var baudRate))
                    BaudRate = baudRate;
            }
        }
    }

  public class EoM3000Plugin : AsyncPluginBase
{
    public EoM3000PluginSettings Settings { get; }

    public EoM3000Plugin(EoM3000PluginSettings settings) => Settings = settings;

    protected override async Task ExecuteAsync(CancellationToken cancellationToken)
    {
        using (var serialPort = new SerialPort(Settings.PortName, Settings.BaudRate))
        {
            serialPort.Open();
            Logger.Info($"Connected to serial port: {Settings.PortName}");

            while (!cancellationToken.IsCancellationRequested)
            {
                await Task.Delay(100, cancellationToken);
                if (serialPort.BytesToRead > 0)
                {
                    var data = new byte[serialPort.BytesToRead];
                    serialPort.Read(data, 0, data.Length);

                    string dataString = Encoding.UTF8.GetString(data);

                    // Split the data string by newline
                    string[] lines = dataString.Trim().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);

                    foreach (string line in lines)
                    {
                        // Split the line by comma
                        string[] values = line.Split(',');

                        if (values.Length == 7)
                        {
                            double arousal;
                            if (double.TryParse(values[1], out arousal))
                            {
                                arousal = Math.Clamp(arousal, 0, 600);
                                var factor = Math.Pow(arousal < 200 ? 0 : MathUtils.Clamp01((arousal - 200) / 400), 4);
                                var secondsPerStroke = MathUtils.Lerp(0.001, 10, factor);

                                InvokeAction<DeviceAxis, double>("Axis::SpeedLimitSecondsPerStroke::Set", DeviceAxis.Parse("L0"), secondsPerStroke);
                                Logger.Info("Arousal: {0}, SecondsPerStroke: {1}", arousal, secondsPerStroke);
                            }
                            else
                            {
                                Logger.Info("Failed to parse arousal value from received data.");
                            }
                        }
                        else
                        {
                            Logger.Info("Received data does not have the expected number of values.");
                        }
                    }
                }
            }
        }
    }
}
}

I have yet to try to tweak the math, but at least now I have a reliable way to read the data. You may need to change your COM port as well. that can be done from UI.

Now that I can do some experimenting I see now that the way MFP handles vibrators doesn’t quite work with this. While the plugin should be perfectly serviceable for stoker devices, for vibrators it merely stops the stroke, and because vibrators are converting position to speed this means that, in some cases, when arousal hits your threshold it will stop the “stroke” at position 100 or 90 etc, which when using a vibrator is the opposite of what you want to stop yourself going over the edge.

A solution to this is to have MFP set the stroke position to 0 (or 100 for inverted vibe scripts) when arousal threshold is met. I don’t know enough about the inner workings of MFP and how it interacts with scripts to know if that’s a possibility or how to implement it.

Another thing about MFP is that it never seems to turn vibrators off completely when at position 0 or paused. probably because its a stroker based app where it it’s told to hold last position. Position 100 while inverted will turn off the motor.

It should stop the vibe just fine when axis value is 0, unless its some device specific behavior. I have lovense one and it stops on 0.

You should use separate vibe (V0/V1) axis for your vibes, dont map everything to L0 in buttplug output. Use the link axis feature and link V0 to L0 so that the script is copied.

If you want it to stop on pause then enable auto-home on vibe axis and set it to 0s duration with short delay. You can also try 0s delay but with auto-home inside script disabled.

In the plugin when you detect that the stroke/vibe axis should stop then you can invoke the Bypass::All action, and the auto-home will trigger after the specified delay.
But I’ll also change the Axis::SpeedLimitSecondsPerStroke::Set behavior so that you can send a value that basically mimics the bypass so you don’t have to use two actions.