Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Pico W, Pico 2 W wi-fi connections, NTP sources for RTC #416

Draft
wants to merge 35 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7af5657
Add StringConfigPoint
chrisib Feb 13, 2025
3bfdb87
Add initial wifi connection implementation
chrisib Feb 13, 2025
7f5e770
Add wifi, NTP configurations
chrisib Feb 13, 2025
938485a
Update configuration
chrisib Feb 13, 2025
746b115
Add initial NTP implementation for realtime clock support
chrisib Feb 13, 2025
97ed1f8
Add additional module dependencies needed for NTP. Catch import error…
chrisib Feb 13, 2025
4e3258c
Formatting
chrisib Feb 13, 2025
5a2e7f4
Newline
chrisib Feb 13, 2025
1f3171e
Update the programming instructions for all pico-family variants, not…
chrisib Feb 14, 2025
0e522ab
Start implementing a basic HTTP server and a server-based control script
chrisib Feb 14, 2025
b0fa79f
Formatting
chrisib Feb 14, 2025
100a955
Show the ip address, netmask, and gateway on the screen
chrisib Feb 14, 2025
29125e7
Set the default SSID and password since we default to AP mode
chrisib Feb 14, 2025
9d5ef80
Add a wait for the connection to become active
chrisib Feb 14, 2025
714ec4d
Remove the optional fields from datetime tuples; make length consiste…
chrisib Feb 14, 2025
5d8b7fa
Add more properties to the wifi connection object
chrisib Feb 14, 2025
00a89e9
Add a default request handler that just raises a NotImplemented error
chrisib Feb 14, 2025
34c7251
Set the socket to non-blocking, loop when checking requests in case t…
chrisib Feb 14, 2025
017a264
Import europi, use fully qualified name for wifi connection
chrisib Feb 14, 2025
a8ca96d
Import the wifi module so we can raise the exception properly
chrisib Feb 14, 2025
47d95b6
Formatting
chrisib Feb 14, 2025
fcb0614
Add styling to the error page, change HTTP error codes to integers, a…
chrisib Feb 16, 2025
c1fffd0
Formatting
chrisib Feb 16, 2025
b654646
Use {{ and }} for the CSS in the page template
chrisib Feb 16, 2025
0044770
Add a comment block explaining how to work with the HttpServer class
chrisib Feb 16, 2025
04dd735
Documentation for the error code blocks
chrisib Feb 16, 2025
b67a001
Add basic HTML page with sliders for the CVs
chrisib Feb 16, 2025
e5b66a8
Add convenience methods for sending JSON data, including a dedicated …
chrisib Feb 16, 2025
15dcca4
Formatting
chrisib Feb 16, 2025
0410b66
Superfluous newline
chrisib Feb 16, 2025
3ac2e61
Separate the request handling into GET and POST. Add some extra code …
chrisib Feb 16, 2025
6cd3dfa
Formatting
chrisib Feb 16, 2025
114f296
Add a new convenience function to send HTML documents
chrisib Feb 16, 2025
2552ac2
Use the new function to serve the control page
chrisib Feb 16, 2025
d60d69b
Formatting
chrisib Feb 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 40 additions & 18 deletions software/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ default configuration:
}
```

System options:
## System

Options:
- `EUROPI_MODEL` specifies the type of EuroPi module. Currently only `"europi"` is supported. Default: `"europi"`
- `PICO_MODEL` must be one of
- `"pico"`,
Expand All @@ -41,7 +43,9 @@ System options:
- `MENU_AFTER_POWER_ON` is a boolean indicating whether or not the module should always return to the main menu when
it powers on. By default the EuroPi will re-launch the last-used program instead of returning to the main menu. Default: `false`

Display options:
## Display

Options:
- `ROTATE_DISPLAY` must be one of `false` or `true`. Default: `false`
- `DISPLAY_WIDTH` is the width of the screen in pixels. The standard EuroPi screen is 128 pixels wide. Default: `128`
- `DISPLAY_HEIGHT` is the height of the screen in pixels. The standard EuroPi screen is 32 pixels tall. Default: `32`
Expand All @@ -50,14 +54,18 @@ Display options:
- `DISPLAY_CHANNEL` is the I²C channel used for the display, either 0 or 1. Default: `0`
- `DISPLAY_CONTRAST` is a value indicating the display contrast. Higher numbers give higher contrast. `0` to `255`. Default: `255`

External I²C options:
## External I²C

Options:
- `EXTERNAL_I2C_SDA` is the I²C SDA pin used for the external I²C interface. Only SDA capable pis can be selected. Default: `2`
- `EXTERNAL_I2C_SCL` is the I²C SCL pin used for the external I²C interface. Only SCL capable pins can be selected. Default: `3`
- `EXTERNAL_I2C_CHANNEL` is the I²C channel used for the external I²C interface, either 0 or 1. Default: `1`
- `EXTERNAL_I2C_FREQUENCY` is the I²C frequency used for the external I²C interface. Default: `100000`
- `EXTERNAL_I2C_TIMEOUT` is the I²C timeout in milliseconds for the external I²C interface. Default: `1000`

I/O voltage options:
## I/O voltage

Options:
- `MAX_OUTPUT_VOLTAGE` is a float in the range `[0.0, 10.0]` indicating the maximum voltage CV output can generate. Default: `10.0`
The hardware is capable of 10V maximum
- `MAX_INPUT_VOLTAGE` is a float in the range `[0.0, 12.0]` indicating the maximum allowed voltage into the `ain` jack.
Expand All @@ -71,36 +79,50 @@ limits are intended for broad compatibility configuration, not for precise tunin
If you assembled your module with the Raspberry Pi Pico 2 (or a clone featuring the RP2350 microcontroller) make sure to
set the `PICO_MODEL` setting to `"pico2"`.


# Experimental configuration

Other configuration properties are used by [experimental features](/software/firmware/experimental/__init__.py)
and can be set using a similar static configuration file. This file is located at `/config/ExperimentalConfig.json`
on the Raspberry Pi Pico. If this file does not exist, default settings will be loaded. The following
shows the default configuration:
on the Raspberry Pi Pico. If this file does not exist, default settings will be loaded.

```json
{
"VOLTS_PER_OCTAVE": 1.0,
"RTC_IMPLEMENTATION": "",
"UTC_OFFSET_HOURS": 0,
"UTC_OFFSET_MINUTES": 0,
}
```
## Quantization

Quantization options:
Options:
- `VOLTS_PER_OCTAVE` must be one of `1.0` (Eurorack standard) or `1.2` (Buchla standard). Default: `1.0`

RTC options:
## Realtime Clock (RTC)

Options:
- `RTC_IMPLEMENTATION` is one of the following, representing the realtime clock enabled on your module:
- `""`: there is no RTC present. (default)
- `"ds3231"`: use a DS3231 module connected to the external I2C interface
- `"ds1307"`: use a DS1307 module connected to the external I2C interface (THIS IS UNTESTED! USE AT YOUR OWN RISK)
- `"ntp"`: use an NTP source as the external clock. Requires wifi-supported Pico and valid network configuration
(see WiFi connection below)
- `NTP_SERVER`: if `RTC_IMPLEMENTATION` is `ntp`, sets the NTP server to use as a clock source.
Default: `0.pool.ntp.org`.

## Timezone

Timezone options:
Options:
- `UTC_OFFSET_HOURS`: The number of hours ahead/behind UTC the local timezone is (-24 to +24)
- `UTC_OFFSET_MINUTES`: The number of minutes ahead/behind UTC the local timezone is (-59 to +59)

## WiFi Connection

Options:
- `WIFI_MODE`: the wireless operation mode, one of:
- `"access_point"` (default): EuroPi acts as a wireless access point for other devices to connect to
-`"client"`: connect EuroPi to an external wireless router or accesspoint (DHCP required)
- `WIFI_SSID`: the SSID of the wireless network to connect to (in `client` mode) or to broadcast
(in `access_point` mode). Default: `"EuroPi"`
- `WIFI_BSSID`: the optional BSSID of the network to connect to (e.g. access point MAC address). Default: `""`
- `WIFI_PASSWORD`: the password of the wireless network. Default: `"europi"`
- `WIFI_CHANNEL`: the WiFi channel 1-13 to use in `access_point` mode; ignored in `client` mode. Default: `10`

WiFi options are only applicable if EuroPi has the Raspberry Pi Pico W or Raspberry Pi Pico 2 W board;
other Pico models do not contain wireless support

# Accessing config members in Python code

The firmware converts the JSON file into a `ConfigSettings` object, where the JSON keys are converted
Expand Down
182 changes: 182 additions & 0 deletions software/contrib/http_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Copyright 2025 Allen Synthesis
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Serves a simple HTTP page to control the levels of the six CV outputs from a browser

Requires a Pico W or Pico 2 W with a valid wifi setup to work
"""

from europi import *
from europi_script import EuroPiScript

from experimental.http import *

HTML_DOCUMENT = """<!DOCTYPE html>
<html lang="en">
<head>
<style>
body {
font-family: Montserrat;
text-align: center;
}
h1 {
font-weight: normal;
font-size: 2.5rem;
letter-spacing: 1.75rem;
padding-left: 1.75rem;
text-align: center;
}
h2 {
font-weight: bold;
font-size: 3.0rem;
}
td {
font-weight: lighter;
font-size: 2.0rem;
}
.content-wrapper {
margin: 0;
position: absolute;
top: 50%;
align-content: center;
width: 100%;
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}
table {
margin: auto;
}
</style>
<title>
EuroPi Web Control
</title>
<script type="text/javascript">
function on_change() {
cvs = {
"cv1": parseFloat(document.getElementById("cv1").value),
"cv2": parseFloat(document.getElementById("cv2").value),
"cv3": parseFloat(document.getElementById("cv3").value),
"cv4": parseFloat(document.getElementById("cv4").value),
"cv5": parseFloat(document.getElementById("cv5").value),
"cv6": parseFloat(document.getElementById("cv6").value)
}
console.debug(cvs)

var xhr = new XMLHttpRequest();
var url = document.URL;
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type", "text/json");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var json = JSON.parse(xhr.responseText);
console.log(json);
}
};
var data = JSON.stringify(cvs);
xhr.send(data);
}
</script>
</head>
<body>
<h1>EuroPi Web Control</h1>
<div class="content-wrapper"?>
<table>
<tr>
<td>
CV1
</td>
<td>
CV2
</td>
<td>
CV3
</td>
</tr>
<tr>
<td>
<input type="range" min="0" max="10" step="0.001" value="0" id="cv1" oninput="on_change()">
</td>
<td>
<input type="range" min="0" max="10" step="0.001" value="0" id="cv2" oninput="on_change()">
</td>
<td>
<input type="range" min="0" max="10" step="0.001" value="0" id="cv3" oninput="on_change()">
</td>
</tr>
<tr>
<td>
CV4
</td>
<td>
CV5
</td>
<td>
CV6
</td>
</tr>
<tr>
<td>
<input type="range" min="0" max="10" step="0.001" value="0" id="cv4" oninput="on_change()">
</td>
<td>
<input type="range" min="0" max="10" step="0.001" value="0" id="cv5" oninput="on_change()">
</td>
<td>
<input type="range" min="0" max="10" step="0.001" value="0" id="cv6" oninput="on_change()">
</td>
</tr>
</table>
</div>
</body>
</html>"""


class HttpControl(EuroPiScript):

def __init__(self):
super().__init__()

self.server = HttpServer(80)

@self.server.get_handler
def handle_get(connection=None, request=None):
self.server.send_html(
connection,
HTML_DOCUMENT,
)

@self.server.post_handler
def handle_post(connection=None, request=None):
# TODO: read the request JSON and set the output CV levels
self.server.send_current_state_json()

def main(self):
if wifi_connection is None:
raise WifiError("No wifi connection")

while not wifi_connection.is_connected:
oled.centre_text(f"""{wifi_connection.ssid}
Waiting for
connection...""")

oled.centre_text(f"""{wifi_connection.ssid}
{wifi_connection.ip_addr}
Connected""")

while True:
self.server.check_requests()


if __name__ == "__main__":
HttpControl().main()
38 changes: 34 additions & 4 deletions software/firmware/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ def validate(self, value) -> Validation:
raise NotImplementedError


class StringConfigPoint(ConfigPoint):
"""A `ConfigPoint` that allows the input of arbitrary strings

:param name: The name of this `ConfigPoint`, will be used by scripts to lookup the configured value.
:param type: The name of this ConfigPoint's type
:param default: The default value
:param danger: If true, mark this option as dangerous to modify in the config editor
"""

def __init__(self, name: str, default: str, danger: bool = False):
super().__init__(name=name, type=float, default=default, danger=danger)

def validate(self, value) -> Validation:
"""Validates the given value with this ConfigPoint. Returns a `Validation` containing the
validation result, as well as an error message containing the reason for a validation failure.
"""
return type(value) is str


class FloatConfigPoint(ConfigPoint):
"""A `ConfigPoint` that requires the selection from a range of floats. The default value
must lie within the specified range
Expand Down Expand Up @@ -138,7 +157,7 @@ class ChoiceConfigPoint(ConfigPoint):
:param danger: If true, mark this option as dangerous to modify in the config editor
"""

def __init__(self, name: str, choices: "List", default, danger: bool = False):
def __init__(self, name: str, choices: list, default, danger: bool = False):
if default not in choices:
raise ValueError("default value must be available in given choices")
super().__init__(name=name, type="choice", default=default, danger=danger)
Expand Down Expand Up @@ -175,7 +194,7 @@ def boolean(name: str, default: bool, danger: bool = False) -> BooleanConfigPoin
return BooleanConfigPoint(name=name, default=default, danger=danger)


def choice(name: str, choices: "List", default, danger: bool = False) -> ChoiceConfigPoint:
def choice(name: str, choices: list, default, danger: bool = False) -> ChoiceConfigPoint:
"""A helper function to simplify the creation of ChoiceConfigPoints. Requires selection from a
limited number of choices. The default value must exist in the given choices.

Expand Down Expand Up @@ -221,13 +240,24 @@ def integer(
)


def string(name: str, default: str, danger: bool = False) -> StringConfigPoint:
"""A helper function to simplify the creation of StringConfigPoints. Allows the input of
any arbitrary string

:param name: The name of this `ConfigPoint`, will be used by scripts to lookup the configured value.
:param default: The default value
:param danger: If true, mark this option as dangerous to modify in the config editor
"""
return StringConfigPoint(name=name, default=default, danger=danger)


class ConfigSpec:
"""
A container for `ConfigPoints` representing the set of configuration options for a specific
script.
"""

def __init__(self, config_points: "List[ConfigPoint]") -> None:
def __init__(self, config_points: list[ConfigPoint]) -> None:
self.points = {}
for point in config_points:
if point.name in self.points:
Expand All @@ -240,7 +270,7 @@ def __len__(self):
def __iter__(self):
return iter(self.points.values())

def default_config(self) -> "dict(str, any)":
def default_config(self) -> dict[str, any]:
"""Returns the default configuration for this spec."""
return {point.name: point.default for point in self.points.values()}

Expand Down
14 changes: 13 additions & 1 deletion software/firmware/europi.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@
from configuration import ConfigSettings
from framebuf import FrameBuffer, MONO_HLSB

from europi_config import load_europi_config, CPU_FREQS
from europi_config import load_europi_config, CPU_FREQS, MODEL_PICO_2W, MODEL_PICO_W
from europi_display import Display, DummyDisplay

from experimental.experimental_config import load_experimental_config
from experimental.wifi import WifiConnection, WifiError


if sys.implementation.name == "micropython":
Expand Down Expand Up @@ -716,5 +717,16 @@ def value(self):
# e.g. to lower power consumption on a very power-constrained system
freq(CPU_FREQS[europi_config.PICO_MODEL][europi_config.CPU_FREQ])

# Connect to wifi, if supported
if europi_config.PICO_MODEL == MODEL_PICO_W or europi_config.PICO_MODEL == MODEL_PICO_2W:
try:
wifi_connection = WifiConnection()
except WifiError as err:
print(err)
wifi_connection = None
else:
wifi_connection = None


# Reset the module state upon import.
reset_state()
Loading
Loading