Skip to content

Commit

Permalink
Merge pull request #42 from CrashOverride85/dev
Browse files Browse the repository at this point in the history
From dev branch for v1.6
  • Loading branch information
CrashOverride85 authored Aug 12, 2023
2 parents 044758f + 34c7f38 commit d1d6184
Show file tree
Hide file tree
Showing 89 changed files with 1,868 additions and 773 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
The ZC95 is a DIY four channel EStim box with similar form factor & output design to the MK312-BT (which in turn is a clone of the ET-312B).
Unlike the 312B, it uses 2x Raspberry Pico microcontrollers instead of an ATMEGA16, and the firmware is open source and mostly written in C++.

If a Pico-W is used for the main MCU, it can be controlled remotely via a Python GUI, and run Lua scripts uploaded to it.
The box can be controlled remotely via a Python GUI, and run Lua scripts uploaded to it, either using RS232 serial or WiFi if a Pico-W is used for the main MCU.

Compared to an MK312-BT, it has 2 extra channels, two trigger inputs (think predicament bondage), and an accessory port. It is missing bluetooth and, _so far_, most of the patterns of the 312. Audio input is possible with an extra/optional board.

Expand Down
14 changes: 11 additions & 3 deletions docs/Build.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ If any of the I2C devices aren't detected, you should see an error similar to th
Here, it can't find the two ICs on the front panel (in this case, the IDC cable was unplugged).
There is serial debugging output on the "Accessory" DB9 connector (tx pin 3, ground pin 5) on the front panel at RS232 levels.

### Clearing saved settings / EEPROM
So far I've never found it necessary, but the EEPROM can be reset to defaults by holding down the top right button and powering the box on. This will show a confirmation screen asking if it should be reset, then pressing bottom left button will clear it.
Once reset, there should be an "EEPROM cleared" message, followed by flashing red lights. Power cycle the box, and it will have been reset to defaults.

Before showing the confirmation screen, the box will have confirmed the EEPROM IC can be detected, but no saved settings will have been used.

EEPROM is used to store all settings that can be changed via the menus, but it does not store any uploaded Lua scripts - these are stored in flash, and can be wiped by reuploading the firmware.

## ZC624 output module
There is also debugging output from the ZC624 board on the serial header. Note that is at 3v3 level, and RS232 levels would damage it.

Expand Down Expand Up @@ -230,7 +238,7 @@ The process for self power-on calibration (for each channel) is:

All this means is that an error like this:
```
calibrate for sm=3 FAILED! final voltage = 0.011279, dac_value = 2600 (expecting 0.075v - 0.090v)
calibrate for sm=3 FAILED! final voltage = 0.011279, dac_value = 2400 (expecting 0.075v - 0.090v)
```
means that the PFET never switched on (enough) to complete calibration successfully.

Expand All @@ -241,9 +249,9 @@ calibrate for sm=3 FAILED! final voltage = 1.541235, dac_value = 3400 (expecting
means the opposite - at the starting value of 3400, the voltage across the sense resistor (and therefore current flow though the PFET & transformer) was already way above what it should be.

Possible causes (not exhaustive!) for calibration to fail:
* If all channels are showing a similar and very low voltage (~0.01v) at a DAC value of 2600, suspect the 9v supply (and in turn, the 12v supply it's derived from)
* If all channels are showing a similar and very low voltage (~0.01v) at a DAC value of 2400, suspect the 9v supply (and in turn, the 12v supply it's derived from)
* Bad/incorrect PFET - e.g. not an IRF9Z24**NPBF**
* Too low value sense resistor (if DAC value is 2600), or too high (if DAC value is 3400)
* Too low value sense resistor (if DAC value is 2400), or too high (if DAC value is 3400)
* Incorrect resistor value in the opamp circuit - likely if the final voltage is wildly off. Also suspect a bad/cracked resistor or poor solder joint if the final voltage keeps changing between power cycles


Expand Down
11 changes: 9 additions & 2 deletions docs/LuaNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Config = {
{
type = "MIN_MAX",
title = "Delay",
group = 0,
id = 1,
min = 100,
max = 2000,
Expand All @@ -40,6 +41,7 @@ Config = {
{
type = "MULTI_CHOICE",
title = "Output",
group = 0,
id = 2,
choices = {
{choice_id = 1, description = "Pulse"},
Expand Down Expand Up @@ -121,6 +123,7 @@ Config = {
type = "MIN_MAX",
title = "Delay",
id = 1,
group = 0,
min = 100,
max = 2000,
increment_step = 100,
Expand All @@ -131,6 +134,7 @@ Config = {
type = "MULTI_CHOICE",
title = "Output",
id = 2,
group = 0,
choices = {
{choice_id = 1, description = "Pulse"},
{choice_id = 2, description = "Constant"}
Expand All @@ -141,7 +145,10 @@ Config = {
```
`name = "Toggle"` sets the name for the script - this is prefixed with `U:` then used on the patterns menu.

A menu entry is displayed when the script is running for each item in `menu_items`; each must be given a unique id, numbered sequentially from 1. There are two types supported:
A menu entry is displayed when the script is running for each item in `menu_items`; each must be given a unique id, numbered sequentially from 1.
_Optionally_, each item can be given a `group` number - this only has any affect when ran remotely using the GUI, and allows related options to be group together, instead of appearing in one long list (useful for scripts with many options).

There are two types supported:
* `MIN_MAX` - shows a horizontal bar graph that can be changed between the set min/max using the adjust dial. The unit of measure (uom) text is displayed as suffix to the numeric value in the bar chart
* `MULTI_CHOICE` - used to show a menu option that allows for one of multiple settings to be picked. Each choice must have a unique id.

Expand Down Expand Up @@ -304,7 +311,7 @@ Called when a `MIN_MAX` type pattern option is changed, and is called with the m
Called when a `MULTI_CHOICE` type pattern option is changed, and is called with the menu ID of the option, and the ID of the selected choice.

### SoftButton(pushed)
Called with `pushed=True` when the top left soft button is pressed, and then again when it is released with `pushed=False`.
Called with `pushed=True` when the top left soft button is pressed, and then again when it is released with `pushed=False`. The soft button text is set by specifying `soft_button = "<label>"` in the `Config = {}` section. See `fire.lua` script for an example.

### ExternalTrigger(socket, part, active)
Called when an external trigger happens.
Expand Down
45 changes: 35 additions & 10 deletions docs/RemoteAccess.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,26 @@
***Warning***: All remote access and Lua stuff is somewhat experimental at this point

## Summary
If a Pico-W is used for the MCU on the main board, a "Remote access" menu is available in the config menu.
The ZC95 can be controlled remotely either using RS232 serial (Aux port) or using WiFi if a Pico-W is used for the MCU on the main board.

The remote access options can be used to:
* Upload Lua scripts (of which 5 can be stored)
* Control the box remotely with a Python GUI

At present, other than the initial WiFi setup, there is no web interface (yet).
## Connecting using WiFi
If a Pico-W is used, the Config -> Remote Access menu should include options relating to WiFi.

At present, other than the initial WiFi setup, there is no web interface (yet) - all control is via python scripts ran on a PC.

## Configuring Wifi
The basic process is use the "Config Wifi/AP mode" option, ideally connect to the ZC95 using a phone, then use the web interface to enter a WiFi SSID/Password. When the "Connect to Wifi" option is next used, these credentials will be used. At present, other than setting the WiFi SSID/password, nothing else can be done in ap mode.

When selecting "Config Wifi/AP mode", after a brief "Starting" message, you should be presented with a screen that looks something like:
When selecting "Config Wifi/AP mode", a screen showing the strongest networks found is displayed:

![Scanning]

This will continue to scan for networks for as long as the screen is open.

When at least one WiFi network is detected, the "Start AP" soft button (top right) is enabled. Pressing this starts the Access Point mode, and shows a QR code to allow connecting:

![QrCode]

Expand All @@ -35,13 +44,27 @@ If the config page doesn't automatically appear, manually browse to "http://192.
### Android
Untested, but hopefully similar.

## Connecting to Wifi
### Connecting
Select the "Connect to WiFi" option, and it should connect to the WiFi network previously configured using the above steps. Once connected, you should see something that looks like:

![ConnectedToWifi]

At this point, the Python GUI and scripts to list/upload Lua scripts should work if given the IP displayed on screen.

## Serial control
The ZC95 can also be controlled using RS-232 serial via the Aux port. Pin out:

* Tip = Transmit (ZC95 output)
* Ring = Receive
* Sleeve = Ground

Before this mode can be used, the hardware configuration needs to be set to:
* Debug output = `Accessory port` or `Off`
* Aux port use = `Serial I/O`

(see Hardware config section in [Operation notes](./Operation.md))

Once selected, and screen is showing "Serial control mode", the Python GUI and scripts to list/upload Lua scripts should work if given the serial port the ZC95 is connected to.

## Python scripts
There are a few Python scripts available in the `remote_access` folder for interacting with the ZC95 once it's connected to wifi:
Expand All @@ -50,7 +73,9 @@ There are a few Python scripts available in the `remote_access` folder for inter
* lua_upload.py - uploads a Lua script
* pattern_gui.py - starts a GUI that starts a pattern and can then be used to control it

(all scripts can be given an optional `--debug` parameter which shows messages being sent/received, and sometimes other extra info)
For all scripts:
* Either ``--ip <IP address>`` _or_ ``--serial <serial port>`` must be specified
* The optional `--debug` parameter can be used to show messages being sent/received, and sometimes other extra info

### pattern_list.py
Used to list all pattens on the ZC95 that can be controlled remotely, including any from Lua scripts uploaded. E.g.:
Expand Down Expand Up @@ -82,16 +107,16 @@ Finally, the Audio patterns are excluded for the time being as these do not work
### lua_manage.py
Can be used to list and delete Lua scripts on the box, e.g.:
```
$ python3 lua_manage.py --ip 192.168.1.137 --list
Connecting
$ python3 lua_manage.py --serial /dev/ttyUSB0 --list
Opening: /dev/ttyUSB0
Connection opened
Script slots on ZC95:
5 - <empty>
4 - <empty>
3 - <empty>
2 - <empty>
1 - U:Waves
Websocket connection closed
Connection closed
$
```
When uploading a Lua script, it's the id/index from the above list that needs to be used to specify where to put the script.
Expand Down Expand Up @@ -165,7 +190,7 @@ If started with the ```--debug``` flag, in addition to showing messages sent/rec
In this case, it's showing a script that's failed on line 112 due to a call to a function that doesn't exist. Any `print()` output from Lua scripts will also appear here, making this mode useful for testing new Lua scripts.



[Scanning]: images/screen_ra_scanning.jpg "Remote access (AP mode) scanning screen"
[QrCode]: images/screen_ra_qr.jpg "Remote access (AP mode) screen showing QR code"
[PhoneConfigWifi]: images/phone_config_wifi.jpg "Configure wifi on iphone"
[ShowSsidAndPassword]: images/screen_ra_ap.jpg "Screen showing SSID and password for AP mode"
Expand Down
Binary file added docs/images/screen_ra_scanning.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
116 changes: 116 additions & 0 deletions remote_access/lib/ZcSerial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import time
from serial import Serial
from serial.threaded import ReaderThread, Protocol, LineReader
import threading
import json
import sys
import queue

class SerialReader(Protocol):
on_receive = None
on_open = None
on_close = None

def __init__(self):
self.__state = 'IDLE'
self.__message = bytearray()

def connection_made(self, transport):
self.on_open()

def connection_lost(self, exc):
self.on_close()

def data_received(self, data):
for byte in data:
if self.__state == "IDLE":
if byte == 0x02: # STX
self.__state = "RECV"

elif self.__state == "RECV":
if byte == 0x03: # ETX
message_string = self.__message.decode("utf-8")

self.on_receive(message_string)

# reset for next message
self.__message = bytearray()
self.__state = "IDLE"

else:
self.__message.append(byte)


class ZcSerial:
def __init__(self, serial_port, rcv_queue, debug):
print("Opening: " + serial_port)
self.serial = Serial(serial_port, baudrate=115200, timeout=0)

serial_reader = SerialReader
serial_reader.on_receive = self.__on_message
serial_reader.on_open = self.__on_open
serial_reader.on_close = self.__on_close

self.__recv_waiting = False
self.__pending_recv_message = ""
self.__recv_event = threading.Event()
self.__waiting_for_msgId = 0
self.__connection_wait_event = threading.Event()
self.__rcv_queue = rcv_queue
self.debug = debug

self.__reader = ReaderThread(self.serial, serial_reader)
self.__reader.start()

def __on_message(self, message):
if self.debug:
print("< " + message)

result = json.loads(message)
if self.__recv_waiting and "MsgId" in result and result["MsgId"] == self.__waiting_for_msgId:
self.__pending_recv_message = message
self.__recv_event.set()
else:
self.__rcv_queue.put(result)

def __on_close(self):
print("Connection closed")

def __on_open(self):
print("Connection opened")
self.__connection_wait_event.set()

def wait_for_connection(self):
self.__connection_wait_event.wait()

def send(self, message):
if self.debug:
print("> " + message)

self.serial.write(b'\x02') # STX
self.serial.write(message.encode('utf-8'))
self.serial.write(b'\x03') # ETX

# note: not at all thread safe
def recv(self, msgId):
self.__waiting_for_msgId = msgId
self.__recv_waiting = True

if self.__recv_event.wait(timeout=2): # timeout is seconds
retval = self.__pending_recv_message
else:
retval = None

self.__recv_waiting = False
self.__recv_event.clear()
self.__waiting_for_msgId = 0

return retval

def run_forever(self):
pass # might do some sort of ping here one day to check connection is still good

def stop(self):
self.serial.write(b'\x04') # EOT, causes the zc95 to stop a pattern if one is running, and reset the connection (clears any state, etc.)
self.__reader.stop()
self.serial.close()
5 changes: 3 additions & 2 deletions remote_access/lua/fire.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
Config = {
name = "Fire"
name = "Fire",
soft_button = "Fire"
}

function Loop(time_ms)
Expand Down Expand Up @@ -31,7 +32,7 @@ function ExternalTrigger(socket, part, active)
end

function SoftButton(pushed)
AllChannels(pushed)
AllChannels(pushed)
end

function AllChannels(on)
Expand Down
28 changes: 18 additions & 10 deletions remote_access/lua_manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,33 @@
import lib.ZcMessages as zc
import queue
from lib.ZcWs import ZcWs

from lib.ZcSerial import ZcSerial

parser = argparse.ArgumentParser(description='Manage Lua scripts on ZC95')
parser.add_argument('--debug', action='store_true', help='Show debugging information')
parser.add_argument('--ip', action='store', required=True, help='IP address of ZC95')

connection_group = parser.add_mutually_exclusive_group(required=True)
connection_group.add_argument('--ip', action='store', help='IP address of ZC95')
connection_group.add_argument('--serial', action='store', help='Serial port to use')

parser.add_argument('--list', action='store_true', help='List scripts stored on ZC95')
parser.add_argument('--delete', action='store', type=int, choices=range(1, 6), help='Delete script at slot on ZC95')

args = parser.parse_args()


# Websocket setup / connect
rcv_queue = queue.Queue()
zcws = ZcWs(args.ip, rcv_queue, args.debug)
ws_thread = threading.Thread(target=zcws.run_forever)
ws_thread.start()
zcws.wait_for_connection()

zc_messages = zc.ZcMessages(zcws, args.debug)
# Connect either using serial or websocket
if args.serial:
zc_connection = ZcSerial(args.serial, rcv_queue, args.debug)
else:
zc_connection = ZcWs(args.ip, rcv_queue, args.debug)

conn_thread = threading.Thread(target=zc_connection.run_forever)
conn_thread.start()
zc_connection.wait_for_connection()

zc_messages = zc.ZcMessages(zc_connection, args.debug)

if args.list:
scripts = zc_messages.SendGetLuaScripts()
Expand All @@ -37,5 +45,5 @@
else:
print("Done.")

zcws.stop()
zc_connection.stop()

Loading

0 comments on commit d1d6184

Please sign in to comment.