Bary Nusz
Published © GPL3+

Particle Photon Oscilloscope

Using the Photon's built in ADC and TCP capability, you can generate a sub millisecond oscilloscope view on a Windows client.

IntermediateFull instructions provided4,382
Particle Photon Oscilloscope

Things used in this project

Hardware components

Photon
Particle Photon
•Connect any Windows Universal App-Capable Computer to the same local network the Photon is on.
×1

Software apps and online services

Visual Studio 2015
Microsoft Visual Studio 2015

Story

Read more

Schematics

Particle Photon Oscilloscope

Code

Photon initialization

C/C++
In the Photon app, the initialization is done with the following code. You can see we’re setting up a TCP server and starting its service. If you want to use a different port, you can change that here. This is also where we read the device IP address and publish it to the Particle dashboard.
#define BUFFER_SIZE 500
#define ANALOGPIN A0
#define PORT 8007
#define UNKNOWNSIGNALWIDTH 16000
 
TCPClient client;
TCPServer server = TCPServer(PORT);
char myIpAddress[24];
int16_t dataArray[BUFFER_SIZE];
int TSArray[BUFFER_SIZE];
int average = 0;
int minv = 4096;
int maxv = 0;
int sampleDelay = 500;
int offsetCrossingIndexs[2] = {0,0};
int offsetCrossings[2] = {0,0};
 
void setup() {
    IPAddress myIp = WiFi.localIP();
    sprintf(myIpAddress, "%d.%d.%d.%d", myIp[0], myIp[1], myIp[2], myIp[3]);
    Particle.publish("IP",myIpAddress);
    delay(1000);
 
    server.begin();
}

Data capture

C/C++
The first thing we do in the loop function is capture the signal samples, which we’re doing in a for loop. Note that before we enter the signal capture for loop we store the starting timestamp. As we capture each sample, we’re also storing the starting timestamp offset in microseconds for those samples.
void loop() {
    
    unsigned long startMSTS = micros();
    for(int i = 0; i < arraySize(dataArray); i += 1)
    {
        dataArray[i] = analogRead(ANALOGPIN);
        TSArray[i] = micros() - startMSTS;
    }
    
    //Check to see if the micros() overflowed during the sample. If so, try again
    if (TSArray[0] > TSArray[arraySize(dataArray)-1]){
        return;
    }

Signal Processing

C/C++
In the next section of the loop function we’re doing our signal processing. These are the same functions that I coded in my Windows IoT Core Oscilloscope project recoded for the Photon.
    processStats();
    normalizeDataToAverage();
   
    if (!processOffsetCrossings())
    {
        //A clean signal has not been found. 
        Particle.publish("Signal","Unknown");
        offsetCrossingIndexs[0] = 0;
        offsetCrossings[0] = TSArray[0];
        bool unknownSignalFound = false;
        //See if there is at least UNKNOWNSIGNALWIDTH microsecs worth of signal
        for(int i = 1; i < arraySize(dataArray); i += 1)
        {
            if ((TSArray[i] - TSArray[0]) > UNKNOWNSIGNALWIDTH){
                offsetCrossingIndexs[1] = i;
                offsetCrossings[1] = TSArray[i];
                unknownSignalFound = true;
                Particle.publish("Signal",String(TSArray[i]));
                break;
            }
        }
        if (!unknownSignalFound) {
            delay(1000);
            Particle.publish("Signal","Error");
            return;
        }
    }

TCP communication

C/C++
In the last section of the loop function we’re checking to see if there is a client that has connected to our TCP server. If there is a connection, we send our signal data.
    TCPClient check = server.available();
    if (check.connected())
    {
        client = check;
        Particle.publish("Server","Connected");
    }
    if (client.connected())
    {
        write_socket(client, dataArray);
    }

    delay(sampleDelay);
}

Write_socket

C/C++
The write_socket function preps our signal data for writing to our client. We prepend the write buffer with the detected signal trigger offset and the calculated sample rate.
void write_socket(TCPClient socket, int16_t *buffer) 
{
    int tcpIdx = 0;
    uint8_t tcpBuffer[BUFFER_SIZE*2];
 
    //First int is the trigger offset index
    tcpBuffer[tcpIdx] = offsetCrossingIndexs[0] & 0xff;
    tcpBuffer[tcpIdx+1] = (offsetCrossingIndexs[0] >> 8);
    tcpIdx += 2;
    //Second int is the microseconds per sample
    int samplerate = (offsetCrossings[1] - offsetCrossings[0]) / (offsetCrossingIndexs[1] - offsetCrossingIndexs[0]);
    tcpBuffer[tcpIdx] = samplerate & 0xff;
    tcpBuffer[tcpIdx+1] = (samplerate >> 8);
    tcpIdx += 2;
    //The rest of ints is the data array
    for(int i = 0; i < arraySize(dataArray); i += 1)
    {
        tcpBuffer[tcpIdx] = buffer[i] & 0xff;
        tcpBuffer[tcpIdx+1] = (buffer[i] >> 8);
        tcpIdx += 2;
    }
    socket.flush();
    socket.write(tcpBuffer, tcpIdx);
    //Particle.publish("Written",String(tcpIdx));
}

Signal processing functions

C/C++
The signal processing functions are functionally the same as my Windows IoT Core versions.
void processStats()
{
    minv = 4096;
    maxv = 0;
    average = 0;
    float sum = 0;
    for(int i = 0; i < arraySize(dataArray); i += 1)
    {
        sum += dataArray[i];
        if (dataArray[i] < minv){
            minv = dataArray[i];
        }
        if (dataArray[i] > maxv){
            maxv = dataArray[i];
        }
    }
    average = sum / arraySize(dataArray);
    minv = minv - average;
    maxv = maxv - average;
}
 
void normalizeDataToAverage()
{
    for(int i = 0; i < arraySize(dataArray); i += 1)
    {
        dataArray[i] = dataArray[i] - average;
    }
}
 
bool processOffsetCrossings()
{
    offsetCrossingIndexs[0] = 0;
    offsetCrossingIndexs[1] = 0;
    offsetCrossings[0] = 0;
    offsetCrossings[1] = 0;
    bool crossSet = false;
    bool fullCycle = false;
    int crossingIndex = 0;
    int triggerValue = maxv * 0.5;
 
    for(int i = 0; i < arraySize(dataArray); i += 1)
    {
        if (crossingIndex > 1){
            fullCycle = true;
            break;
        }   
        if (!crossSet && dataArray[i] > triggerValue){
            crossSet = true;
        }
        if (crossSet && dataArray[i] < triggerValue){
            offsetCrossingIndexs[crossingIndex] = i;
            offsetCrossings[crossingIndex] = TSArray[i];
            ++crossingIndex;
            crossSet = false;
        }
    }
    return fullCycle;
}

Universal Windows App - TCP Communication

C#
In the Universal Windows App we connect to the Photon TCP server with a TCP socket object. After we establish a connection we start a timer to enter into a display sample data cycle.
_Connect = new DelegateCommand((x) => {
    DnsEndPoint hostEntry = new DnsEndPoint(this.IP, this.Port);
    _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    SocketAsyncEventArgs socketEventArg = new SocketAsyncEventArgs();
    socketEventArg.RemoteEndPoint = hostEntry;
    socketEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(async delegate (object s, SocketAsyncEventArgs e)
    {
        // Retrieve the result of this request
        await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
        {
            this.Status = e.SocketError.ToString();
            RaisePropertyChanged("Connect");
            RaisePropertyChanged("Receive");
            RaisePropertyChanged("Disconnect");
        });
        Receive.Execute(null);
    });
    _socket.ConnectAsync(socketEventArg);
}, (y) => { return _socket == null || (_socket != null && !_socket.Connected); });
_Receive = new DelegateCommand(async (x) =>
{
    await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
    {
        this.Status = "Listening";
    });
    receiveTimer = new Timer(this.Receive_Timer_Tick, null, 0, System.Threading.Timeout.Infinite);
}, (y) => { return _socket != null && _socket.Connected; });

Universal Windows App - Display sample data cycle

C#
The display sample data cycle is controlled by a timer. When data is received the first two Ints are stripped off. First is the trigger offset. The second is the sample rate. The sample rate normally doesn’t change but could if we were to upgrade the system to support selectable sample rates. The rest of the data is copied into an array that can be displayed on our graph. Note how we offset the point index with the trigger index, which keeps the signal lined up on subsequent data refreshes.
private void Receive_Timer_Tick(object state)
{
    receiveTimer.Dispose();  
    bool go = false;          
    if (_socket != null && _socket.Connected)
    {
        if (socketEventArg == null)
        {
            socketEventArg = new SocketAsyncEventArgs();
            socketEventArg.RemoteEndPoint = _socket.RemoteEndPoint;
            socketEventArg.SetBuffer(new Byte[MAX_BUFFER_SIZE], 0, MAX_BUFFER_SIZE);
            socketEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(async delegate (object s, SocketAsyncEventArgs e)
            {
                if (e.SocketError == SocketError.Success && e.BytesTransferred > 0)
                {
                    //First 2 ints contain the trigger index offset and the sample rate.
                    var triggerIndex = (Int16)((e.Buffer[1] << 8) | e.Buffer[0]);
                    var samplerate = (Int16)((e.Buffer[3] << 8) | e.Buffer[2]);
 
                    Int16[] data = new Int16[(e.BytesTransferred - 4) / 2];
                    for (int i = 2; i < e.BytesTransferred / 2; i++)
                    {
                        int bi = i * 2;
                        byte upper = e.Buffer[bi + 1];
                        byte lower = e.Buffer[bi];
                        data[i - 2] = (Int16)((upper << 8) | lower);
                    }
                    int index = -triggerIndex;
 
                    await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                    {
                        this.Trigger = triggerIndex;
                        this.SampleTime = samplerate;
                        this.Status = string.Format("{0} Points Received", data.Length);
                        this.Points = null;
                        this.Points = data.Select(d => new ScatterDataPoint()
                        {
                            XValue = samplerate * index++,
                            YValue = d,
                        });
                    });
                }                        
                else
                {
                    await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                    {
                        this.Status = e.BytesTransferred > 0 ? e.SocketError.ToString() : "No data received";
                    });
                }
 
                _clientDone.Set();
            });
        }
        _clientDone.Reset();
        _socket.ReceiveAsync(socketEventArg);
        go = _clientDone.WaitOne(TIMEOUT_MILLISECONDS);
        if (!go)
        {
            this.Disconnect.Execute(null);
        }
    }
    if (go && _socket != null && _socket.Connected)
    {
        receiveTimer = new Timer(this.Receive_Timer_Tick, null, 0, System.Threading.Timeout.Infinite);
    }
}

Partical Photon Oscilloscope

There are two parts to this software package. Both are included in this GitHub repository. Get your Photon online by following the guide. Open the Particle Apps site, create a new app, and copy/paste the code. Hit verify to compile.

Credits

Bary Nusz

Bary Nusz

8 projects • 32 followers
Nusz Labs “Tinkerer-in-Chief”.

Comments