Compare commits
12 Commits
main
...
update2026
| Author | SHA1 | Date | |
|---|---|---|---|
|
13c0fd32d6
|
|||
|
0333b12fb3
|
|||
|
3197ae1933
|
|||
|
0bd11e7f3b
|
|||
|
4da779a125
|
|||
|
19f2dbd3a0
|
|||
|
719bed65c9
|
|||
|
be438aa1af
|
|||
|
f19e443eb2
|
|||
|
ca1e8ff2bd
|
|||
|
b011d134b9
|
|||
|
be75066549
|
63
Cargo.lock
generated
63
Cargo.lock
generated
@@ -963,6 +963,12 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range-header"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
@@ -1396,6 +1402,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -1936,8 +1952,10 @@ dependencies = [
|
|||||||
"crossterm 0.29.0",
|
"crossterm 0.29.0",
|
||||||
"mpris",
|
"mpris",
|
||||||
"ratatui 0.30.0",
|
"ratatui 0.30.0",
|
||||||
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-http",
|
||||||
"tui-textarea",
|
"tui-textarea",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2779,6 +2797,19 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
@@ -2795,6 +2826,32 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tower-http"
|
||||||
|
version = "0.6.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"http-range-header",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower-layer"
|
name = "tower-layer"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
@@ -2862,6 +2919,12 @@ version = "0.1.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.18"
|
version = "0.3.18"
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ sqlx = { version = "0.8.3", features = ["sqlite", "runtime-tokio"] }
|
|||||||
tokio = { version = "1.36.0", features = ["rt", "macros", "rt-multi-thread"] }
|
tokio = { version = "1.36.0", features = ["rt", "macros", "rt-multi-thread"] }
|
||||||
tui-textarea = "0.7.0"
|
tui-textarea = "0.7.0"
|
||||||
axum = "0.8.3"
|
axum = "0.8.3"
|
||||||
|
tower-http = { version = "0.6.8", features = ["fs"] }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|
||||||
[profile.optimize]
|
[profile.optimize]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
|
|||||||
9
pico_2026/README.md
Normal file
9
pico_2026/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Rate music for Raspberry Pi Pico
|
||||||
|
|
||||||
|
This is the MicroPython project to connect the Raspberry Pi Pico to the rate_music database via HTTP.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
1. Follow the [instructions](https://www.raspberrypi.com/documentation/microcontrollers/micropython.html) to install MicroPython on the Raspberry Pi Pico.
|
||||||
|
2. Install [rshell](https://github.com/dhylands/rshell).
|
||||||
|
3. Modify `SSID` and `Password` in the `main.py` file.
|
||||||
|
4. Copy the files `main.py, rate.py, wifi_client.py` to the Pi Pico.
|
||||||
12
pico_2026/delete_folder.py
Normal file
12
pico_2026/delete_folder.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
def delete_folder(path):
|
||||||
|
for file in os.listdir(path):
|
||||||
|
full_path = path + "/" + file
|
||||||
|
try:
|
||||||
|
os.remove(full_path) # Datei löschen
|
||||||
|
except OSError:
|
||||||
|
delete_folder(full_path) # Falls es ein Ordner ist, rekursiv löschen
|
||||||
|
os.rmdir(path) # Ordner selbst löschen
|
||||||
|
|
||||||
|
delete_folder("/mfrc522") # Beispiel: Löscht den gesamten MFRC522-Ordner
|
||||||
35
pico_2026/main.py
Normal file
35
pico_2026/main.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import wifi_client
|
||||||
|
import rate
|
||||||
|
|
||||||
|
|
||||||
|
from machine import I2C, Pin
|
||||||
|
from ssd1306 import SSD1306_I2C
|
||||||
|
|
||||||
|
# set SSID and PASSWORD variables to the configurations of the laptop access point
|
||||||
|
SSID = 'Oger-fi'
|
||||||
|
PASSWORD = 'Karlfreitag!'
|
||||||
|
SERVER_PORT = 3000
|
||||||
|
SERVER_IP_LOWER = 100
|
||||||
|
SERVER_IP_UPPER = 102
|
||||||
|
|
||||||
|
#SERVER_IP_LOWER = 120
|
||||||
|
#SERVER_IP_UPPER = 123
|
||||||
|
|
||||||
|
# Initialize I2C and OLED
|
||||||
|
i2c = I2C(0, sda=Pin(8), scl=Pin(9), freq=400000)
|
||||||
|
display = SSD1306_I2C(128, 64, i2c)
|
||||||
|
|
||||||
|
# connect to laptop hotspot
|
||||||
|
wifi_client.wifi_connect(SSID, PASSWORD)
|
||||||
|
#wifi_client.search_server(SERVER_IP_LOWER, SERVER_IP_UPPER, SERVER_PORT)
|
||||||
|
#wifi_client.send_request(42)
|
||||||
|
|
||||||
|
|
||||||
|
# define Pins 0-4 for rate buttons (5 buttons)
|
||||||
|
rate_button_pins:list = [0, 1, 2, 3, 4]
|
||||||
|
send_button_pin:int = 6
|
||||||
|
rate.start_rating(rate_button_pins, send_button_pin, display)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
178
pico_2026/rate.py
Normal file
178
pico_2026/rate.py
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
from machine import Pin
|
||||||
|
import wifi_client
|
||||||
|
import time
|
||||||
|
|
||||||
|
# State Constants
|
||||||
|
STATE_USER_ID = 0
|
||||||
|
STATE_RATING = 1
|
||||||
|
|
||||||
|
# Global variables
|
||||||
|
CURRENT_STATE = STATE_USER_ID
|
||||||
|
USER_ID = 0
|
||||||
|
RATING = 0
|
||||||
|
BUTTON_SEND = None
|
||||||
|
RATE_BUTTONS = []
|
||||||
|
DISPLAY = None
|
||||||
|
|
||||||
|
# Variables for Interrupt (ISR) handling and Debouncing
|
||||||
|
pending_rate_button = -1
|
||||||
|
pending_send_button = False
|
||||||
|
last_interrupt_time = 0
|
||||||
|
DEBOUNCE_MS = 250 # Ignore button presses within 250ms of each other
|
||||||
|
|
||||||
|
# Helper to update display based on state
|
||||||
|
def refresh_display(message=None):
|
||||||
|
if not DISPLAY:
|
||||||
|
return
|
||||||
|
|
||||||
|
DISPLAY.fill(0)
|
||||||
|
|
||||||
|
if message:
|
||||||
|
# Show temporary message (Success, Error, Confirmed)
|
||||||
|
DISPLAY.text(message, 10, 30)
|
||||||
|
elif CURRENT_STATE == STATE_USER_ID:
|
||||||
|
# 1. User ID Mode
|
||||||
|
DISPLAY.text("User ID Mode", 15, 0)
|
||||||
|
|
||||||
|
# Display binary representation: _ _ _ _ _
|
||||||
|
binary_str = ""
|
||||||
|
for i in range(5):
|
||||||
|
bit_pos = 4 - i
|
||||||
|
if USER_ID & (1 << bit_pos):
|
||||||
|
binary_str += "X "
|
||||||
|
else:
|
||||||
|
binary_str += "_ "
|
||||||
|
|
||||||
|
DISPLAY.text(binary_str.strip(), 30, 30)
|
||||||
|
|
||||||
|
elif CURRENT_STATE == STATE_RATING:
|
||||||
|
# 3. Rating Mode
|
||||||
|
DISPLAY.text("Select Rating", 10, 0)
|
||||||
|
|
||||||
|
if RATING == 0:
|
||||||
|
rating_text = "_"
|
||||||
|
else:
|
||||||
|
rating_text = str(RATING)
|
||||||
|
|
||||||
|
DISPLAY.text(rating_text, 60, 30)
|
||||||
|
|
||||||
|
DISPLAY.show()
|
||||||
|
print("Updating screen.")
|
||||||
|
|
||||||
|
# initialize a specific Pin as Input with a pull-down resistor
|
||||||
|
def initialize_pin(pin_number:int):
|
||||||
|
button = Pin(pin_number, Pin.IN, Pin.PULL_DOWN)
|
||||||
|
return button
|
||||||
|
|
||||||
|
# Hardware Interrupt: rate button is pressed
|
||||||
|
def rate_button_pressed(pin:Pin) -> None:
|
||||||
|
global pending_rate_button, last_interrupt_time
|
||||||
|
|
||||||
|
print("rate button pressed:", pin)
|
||||||
|
|
||||||
|
|
||||||
|
current_time = time.ticks_ms()
|
||||||
|
# Simple software debounce
|
||||||
|
if time.ticks_diff(current_time, last_interrupt_time) > DEBOUNCE_MS:
|
||||||
|
# Find which button index was pressed
|
||||||
|
for i, button in enumerate(RATE_BUTTONS):
|
||||||
|
if button == pin:
|
||||||
|
pending_rate_button = i
|
||||||
|
break
|
||||||
|
last_interrupt_time = current_time
|
||||||
|
|
||||||
|
# Hardware Interrupt: Send/Confirm button is pressed
|
||||||
|
def sendButton_pressed(pin:Pin) -> None:
|
||||||
|
global pending_send_button, last_interrupt_time
|
||||||
|
|
||||||
|
print("send button pressed:", pin)
|
||||||
|
|
||||||
|
current_time = time.ticks_ms()
|
||||||
|
if time.ticks_diff(current_time, last_interrupt_time) > DEBOUNCE_MS:
|
||||||
|
pending_send_button = True
|
||||||
|
last_interrupt_time = current_time
|
||||||
|
|
||||||
|
# Process the rate button press (Called from the main loop)
|
||||||
|
def process_rate_button(button_index: int):
|
||||||
|
global USER_ID, RATING, CURRENT_STATE
|
||||||
|
|
||||||
|
|
||||||
|
if CURRENT_STATE == STATE_USER_ID:
|
||||||
|
bit_pos = 4 - button_index
|
||||||
|
USER_ID ^= (1 << bit_pos)
|
||||||
|
print(f"Current User ID: {USER_ID}")
|
||||||
|
|
||||||
|
elif CURRENT_STATE == STATE_RATING:
|
||||||
|
RATING = button_index + 1
|
||||||
|
print(f"Current Rating selected: {RATING}")
|
||||||
|
|
||||||
|
refresh_display()
|
||||||
|
|
||||||
|
# Process the send button press (Called from the main loop)
|
||||||
|
def process_send_button():
|
||||||
|
global CURRENT_STATE, USER_ID, RATING
|
||||||
|
|
||||||
|
if CURRENT_STATE == STATE_USER_ID:
|
||||||
|
# 2. Confirmed ID display
|
||||||
|
refresh_display(f"Confirmed: {USER_ID}")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
CURRENT_STATE = STATE_RATING
|
||||||
|
refresh_display()
|
||||||
|
|
||||||
|
elif CURRENT_STATE == STATE_RATING:
|
||||||
|
if RATING == 0:
|
||||||
|
return # Don't send if no rating is selected yet
|
||||||
|
|
||||||
|
# 4. Sending / Success / Error
|
||||||
|
refresh_display("Sending...")
|
||||||
|
|
||||||
|
success = wifi_client.send_request(USER_ID, RATING)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
refresh_display("Success")
|
||||||
|
else:
|
||||||
|
refresh_display("Error")
|
||||||
|
print("Error sending rating.")
|
||||||
|
|
||||||
|
time.sleep(3) # Display status for 3 seconds
|
||||||
|
|
||||||
|
# Reset for next rating
|
||||||
|
USER_ID = 0
|
||||||
|
RATING = 0
|
||||||
|
CURRENT_STATE = STATE_USER_ID
|
||||||
|
refresh_display()
|
||||||
|
|
||||||
|
# start the rating program
|
||||||
|
def start_rating(rate_button_pins:list, button_send_pin:int, oled_display=None) -> None:
|
||||||
|
global RATE_BUTTONS, BUTTON_SEND, DISPLAY
|
||||||
|
global pending_rate_button, pending_send_button
|
||||||
|
|
||||||
|
DISPLAY = oled_display
|
||||||
|
|
||||||
|
# initialize all pins as buttons and attach interrupts
|
||||||
|
RATE_BUTTONS = []
|
||||||
|
for pin_num in rate_button_pins:
|
||||||
|
button = initialize_pin(pin_num)
|
||||||
|
print("Setting up pin:", pin_num)
|
||||||
|
button.irq(trigger=Pin.IRQ_RISING, handler=rate_button_pressed)
|
||||||
|
RATE_BUTTONS.append(button)
|
||||||
|
|
||||||
|
# initialize send button and attach interrupt
|
||||||
|
BUTTON_SEND = initialize_pin(button_send_pin)
|
||||||
|
BUTTON_SEND.irq(trigger=Pin.IRQ_RISING, handler=sendButton_pressed)
|
||||||
|
|
||||||
|
print("System started.")
|
||||||
|
refresh_display()
|
||||||
|
|
||||||
|
# The Main Event Loop - This safely handles the screen updates
|
||||||
|
while True:
|
||||||
|
if pending_rate_button != -1:
|
||||||
|
process_rate_button(pending_rate_button)
|
||||||
|
pending_rate_button = -1 # Reset the flag
|
||||||
|
|
||||||
|
if pending_send_button:
|
||||||
|
process_send_button()
|
||||||
|
pending_send_button = False # Reset the flag
|
||||||
|
|
||||||
|
time.sleep_ms(50) # Short delay to save power and avoid choking the CPU
|
||||||
158
pico_2026/ssd1306.py
Normal file
158
pico_2026/ssd1306.py
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
|
||||||
|
|
||||||
|
from micropython import const
|
||||||
|
import framebuf
|
||||||
|
|
||||||
|
|
||||||
|
# register definitions
|
||||||
|
SET_CONTRAST = const(0x81)
|
||||||
|
SET_ENTIRE_ON = const(0xA4)
|
||||||
|
SET_NORM_INV = const(0xA6)
|
||||||
|
SET_DISP = const(0xAE)
|
||||||
|
SET_MEM_ADDR = const(0x20)
|
||||||
|
SET_COL_ADDR = const(0x21)
|
||||||
|
SET_PAGE_ADDR = const(0x22)
|
||||||
|
SET_DISP_START_LINE = const(0x40)
|
||||||
|
SET_SEG_REMAP = const(0xA0)
|
||||||
|
SET_MUX_RATIO = const(0xA8)
|
||||||
|
SET_COM_OUT_DIR = const(0xC0)
|
||||||
|
SET_DISP_OFFSET = const(0xD3)
|
||||||
|
SET_COM_PIN_CFG = const(0xDA)
|
||||||
|
SET_DISP_CLK_DIV = const(0xD5)
|
||||||
|
SET_PRECHARGE = const(0xD9)
|
||||||
|
SET_VCOM_DESEL = const(0xDB)
|
||||||
|
SET_CHARGE_PUMP = const(0x8D)
|
||||||
|
|
||||||
|
|
||||||
|
# Subclassing FrameBuffer provides support for graphics primitives
|
||||||
|
# http://docs.micropython.org/en/latest/pyboard/library/framebuf.html
|
||||||
|
class SSD1306(framebuf.FrameBuffer):
|
||||||
|
def __init__(self, width, height, external_vcc):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.external_vcc = external_vcc
|
||||||
|
self.pages = self.height // 8
|
||||||
|
self.buffer = bytearray(self.pages * self.width)
|
||||||
|
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
|
||||||
|
self.init_display()
|
||||||
|
|
||||||
|
def init_display(self):
|
||||||
|
for cmd in (
|
||||||
|
SET_DISP | 0x00, # off
|
||||||
|
# address setting
|
||||||
|
SET_MEM_ADDR,
|
||||||
|
0x00, # horizontal
|
||||||
|
# resolution and layout
|
||||||
|
SET_DISP_START_LINE | 0x00,
|
||||||
|
SET_SEG_REMAP | 0x01, # column addr 127 is mapped to SEG0
|
||||||
|
SET_MUX_RATIO,
|
||||||
|
self.height - 1,
|
||||||
|
SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0
|
||||||
|
SET_DISP_OFFSET,
|
||||||
|
0x00,
|
||||||
|
SET_COM_PIN_CFG,
|
||||||
|
0x02 if self.width > 2 * self.height else 0x12,
|
||||||
|
# timing and driving scheme
|
||||||
|
SET_DISP_CLK_DIV,
|
||||||
|
0x80,
|
||||||
|
SET_PRECHARGE,
|
||||||
|
0x22 if self.external_vcc else 0xF1,
|
||||||
|
SET_VCOM_DESEL,
|
||||||
|
0x30, # 0.83*Vcc
|
||||||
|
# display
|
||||||
|
SET_CONTRAST,
|
||||||
|
0xFF, # maximum
|
||||||
|
SET_ENTIRE_ON, # output follows RAM contents
|
||||||
|
SET_NORM_INV, # not inverted
|
||||||
|
# charge pump
|
||||||
|
SET_CHARGE_PUMP,
|
||||||
|
0x10 if self.external_vcc else 0x14,
|
||||||
|
SET_DISP | 0x01,
|
||||||
|
): # on
|
||||||
|
self.write_cmd(cmd)
|
||||||
|
self.fill(0)
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
def poweroff(self):
|
||||||
|
self.write_cmd(SET_DISP | 0x00)
|
||||||
|
|
||||||
|
def poweron(self):
|
||||||
|
self.write_cmd(SET_DISP | 0x01)
|
||||||
|
|
||||||
|
def contrast(self, contrast):
|
||||||
|
self.write_cmd(SET_CONTRAST)
|
||||||
|
self.write_cmd(contrast)
|
||||||
|
|
||||||
|
def invert(self, invert):
|
||||||
|
self.write_cmd(SET_NORM_INV | (invert & 1))
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
x0 = 0
|
||||||
|
x1 = self.width - 1
|
||||||
|
if self.width == 64:
|
||||||
|
# SSD1306 only has 128x64 pixels. For 64x48 displays,
|
||||||
|
# the columns are offset by 32.
|
||||||
|
x0 += 32
|
||||||
|
x1 += 32
|
||||||
|
self.write_cmd(SET_COL_ADDR)
|
||||||
|
self.write_cmd(x0)
|
||||||
|
self.write_cmd(x1)
|
||||||
|
self.write_cmd(SET_PAGE_ADDR)
|
||||||
|
self.write_cmd(0)
|
||||||
|
self.pages - 1
|
||||||
|
self.write_cmd(self.pages - 1)
|
||||||
|
self.write_data(self.buffer)
|
||||||
|
|
||||||
|
|
||||||
|
class SSD1306_I2C(SSD1306):
|
||||||
|
def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
|
||||||
|
self.i2c = i2c
|
||||||
|
self.addr = addr
|
||||||
|
self.temp = bytearray(2)
|
||||||
|
self.write_list = [b"\x40", None] # Co=0, D/C#=1
|
||||||
|
super().__init__(width, height, external_vcc)
|
||||||
|
|
||||||
|
def write_cmd(self, cmd):
|
||||||
|
self.temp[0] = 0x00 # Co=1, D/C#=0
|
||||||
|
self.temp[1] = cmd
|
||||||
|
self.i2c.writeto(self.addr, self.temp)
|
||||||
|
|
||||||
|
def write_data(self, buf):
|
||||||
|
self.write_list[1] = buf
|
||||||
|
self.i2c.writevto(self.addr, self.write_list)
|
||||||
|
|
||||||
|
|
||||||
|
class SSD1306_SPI(SSD1306):
|
||||||
|
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
|
||||||
|
self.rate = 10 * 1024 * 1024
|
||||||
|
dc.init(dc.OUT, value=0)
|
||||||
|
res.init(res.OUT, value=0)
|
||||||
|
cs.init(cs.OUT, value=1)
|
||||||
|
self.spi = spi
|
||||||
|
self.dc = dc
|
||||||
|
self.res = res
|
||||||
|
self.cs = cs
|
||||||
|
import time
|
||||||
|
|
||||||
|
self.res(1)
|
||||||
|
time.sleep_ms(1)
|
||||||
|
self.res(0)
|
||||||
|
time.sleep_ms(10)
|
||||||
|
self.res(1)
|
||||||
|
super().__init__(width, height, external_vcc)
|
||||||
|
|
||||||
|
def write_cmd(self, cmd):
|
||||||
|
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
|
||||||
|
self.cs(1)
|
||||||
|
self.dc(0)
|
||||||
|
self.cs(0)
|
||||||
|
self.spi.write(bytearray([cmd]))
|
||||||
|
self.cs(1)
|
||||||
|
|
||||||
|
def write_data(self, buf):
|
||||||
|
self.spi.init(baudrate=self.rate, polarity=0, phase=0)
|
||||||
|
self.cs(1)
|
||||||
|
self.dc(1)
|
||||||
|
self.cs(0)
|
||||||
|
self.spi.write(buf)
|
||||||
|
self.cs(1)
|
||||||
13
pico_2026/test_webserver.py
Normal file
13
pico_2026/test_webserver.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from flask import Flask, request
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
# Endpoint for receiving ratings
|
||||||
|
@app.route('/ratings/<int:user_id>/<int:rating>', methods=['GET'])
|
||||||
|
def receive_rating(user_id: int, rating: int):
|
||||||
|
print(f"Received rating: User {user_id} -> {rating}")
|
||||||
|
return f"Received rating {rating} for user {user_id}!", 200
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Listen on all interfaces, port 3000 as defined in main.py
|
||||||
|
app.run(host="0.0.0.0", port=3000)
|
||||||
88
pico_2026/wifi_client.py
Normal file
88
pico_2026/wifi_client.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import network
|
||||||
|
import urequests
|
||||||
|
import time
|
||||||
|
import usocket
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
|
||||||
|
# global variable for the IP-Adress of the laptop hotspot
|
||||||
|
IP_ADRESS_SERVER:str = '192.168.0.100' #None
|
||||||
|
#IP_ADRESS_SERVER:str = '10.232.67.121' #None
|
||||||
|
IP_ADRESS_ACCESSPOINT = None
|
||||||
|
SERVER_PORT:int = 3000 #None
|
||||||
|
|
||||||
|
# configures the raspberry pi as wifi-client and connects it to an access point
|
||||||
|
def wifi_connect(SSID:str, PASSWORD:str) -> None:
|
||||||
|
|
||||||
|
# configure raspberry pi as client
|
||||||
|
wifi = network.WLAN(network.STA_IF)
|
||||||
|
# activate client
|
||||||
|
wifi.active(True)
|
||||||
|
# connect to laptop hotspot
|
||||||
|
wifi.connect(SSID, PASSWORD)
|
||||||
|
|
||||||
|
# wait until it has connected
|
||||||
|
while not wifi.isconnected():
|
||||||
|
print("Connect to", SSID, "...")
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# connected to laptop hotspot
|
||||||
|
print("Connected to", SSID)
|
||||||
|
# geat and print IP-adress of raspberry
|
||||||
|
ip_adress_pico = wifi.ifconfig()[0]
|
||||||
|
print("IP-Adress Pico:", ip_adress_pico)
|
||||||
|
# get ip-adress of access point and store it in the global variable
|
||||||
|
global IP_ADRESS_ACCESSPOINT
|
||||||
|
IP_ADRESS_ACCESSPOINT = wifi.ifconfig()[2]
|
||||||
|
print("IP-adress access point:", IP_ADRESS_ACCESSPOINT)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# send a HTTP-request to the access point with the rate value
|
||||||
|
def send_request(user_id:int, rating:int) -> bool:
|
||||||
|
|
||||||
|
# create HTTP-URL
|
||||||
|
URL = f"http://{IP_ADRESS_SERVER}:{str(SERVER_PORT)}/{user_id}/{rating}"
|
||||||
|
print(URL)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# create request
|
||||||
|
response = urequests.get(URL, timeout=5.0)
|
||||||
|
|
||||||
|
# print HTTP-answer of the access point
|
||||||
|
print("Server response:")
|
||||||
|
print(response.text)
|
||||||
|
print()
|
||||||
|
response.close()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Request failed: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# test, if there can be a connection to the given ip adress and port
|
||||||
|
def ping_ip(ip:str, port:int) -> bool:
|
||||||
|
s = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM)
|
||||||
|
s.settimeout(1)
|
||||||
|
try:
|
||||||
|
s.connect((ip, port))
|
||||||
|
print(f"{ip} reachable")
|
||||||
|
s.close()
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
print(f"{ip} not reachable")
|
||||||
|
s.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# search for the server in the lokal network
|
||||||
|
# in the ip range from lower_limit to upper_limit
|
||||||
|
def search_server(lower_limit:int, upper_limit:int, port:int) -> str:
|
||||||
|
global IP_ADRESS_SERVER
|
||||||
|
for i in range(lower_limit, upper_limit):
|
||||||
|
ip = IP_ADRESS_ACCESSPOINT[:-1] + str(i)
|
||||||
|
if ping_ip(ip, port):
|
||||||
|
print(f"Server found unter: {ip}:{port}")
|
||||||
|
IP_ADRESS_SERVER = ip
|
||||||
|
return ip
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
use std::i64;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use serde::Serialize;
|
||||||
use sqlx::{Row, SqlitePool};
|
use sqlx::{Row, SqlitePool};
|
||||||
|
use std::i64;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
pool: SqlitePool,
|
pool: SqlitePool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct UserStats {
|
||||||
|
pub name: String,
|
||||||
|
pub rating_count: i64,
|
||||||
|
pub average_rating: f64,
|
||||||
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub async fn new() -> Result<Self> {
|
pub async fn new() -> Result<Self> {
|
||||||
let pool = SqlitePool::connect("sqlite://ratings.db?mode=rwc").await?;
|
let pool = SqlitePool::connect("sqlite://ratings.db?mode=rwc").await?;
|
||||||
@@ -147,7 +154,8 @@ impl Database {
|
|||||||
song: &str,
|
song: &str,
|
||||||
rating: i64,
|
rating: i64,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut conn = self.pool.acquire().await?;
|
// Begin the transaction
|
||||||
|
let mut tx = self.pool.begin().await?;
|
||||||
|
|
||||||
// Add interpret
|
// Add interpret
|
||||||
let _ = sqlx::query(
|
let _ = sqlx::query(
|
||||||
@@ -157,7 +165,7 @@ impl Database {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(interpret)
|
.bind(interpret)
|
||||||
.execute(&mut *conn)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Get the interpret ID
|
// Get the interpret ID
|
||||||
@@ -168,7 +176,7 @@ impl Database {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(interpret)
|
.bind(interpret)
|
||||||
.fetch_one(&mut *conn)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let interpret_id: i64 = record.try_get("id")?;
|
let interpret_id: i64 = record.try_get("id")?;
|
||||||
@@ -182,7 +190,7 @@ impl Database {
|
|||||||
)
|
)
|
||||||
.bind(song)
|
.bind(song)
|
||||||
.bind(interpret_id)
|
.bind(interpret_id)
|
||||||
.execute(&mut *conn)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Get the song ID
|
// Get the song ID
|
||||||
@@ -193,7 +201,7 @@ impl Database {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(song)
|
.bind(song)
|
||||||
.fetch_one(&mut *conn)
|
.fetch_one(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let song_id: i64 = record.try_get("id")?;
|
let song_id: i64 = record.try_get("id")?;
|
||||||
@@ -208,13 +216,16 @@ impl Database {
|
|||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(song_id)
|
.bind(song_id)
|
||||||
.bind(rating)
|
.bind(rating)
|
||||||
.execute(&mut *conn)
|
.execute(&mut *tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// Commit the transaction to save everything!
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_user_most_ratings(&self) -> Result<()> {
|
pub async fn get_user_most_ratings(&self) -> Result<Vec<UserStats>> {
|
||||||
let mut conn = self.pool.acquire().await?;
|
let mut conn = self.pool.acquire().await?;
|
||||||
|
|
||||||
let records = sqlx::query(
|
let records = sqlx::query(
|
||||||
@@ -229,16 +240,16 @@ impl Database {
|
|||||||
.fetch_all(&mut *conn)
|
.fetch_all(&mut *conn)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
//let id = record.try_get("id")?;
|
// Map the database records into our new struct
|
||||||
|
let mut stats = Vec::new();
|
||||||
for r in records {
|
for r in records {
|
||||||
let name: &str = r.try_get("name")?;
|
stats.push(UserStats {
|
||||||
let count: i64 = r.try_get("c")?;
|
name: r.try_get("name")?,
|
||||||
let avg: f64 = r.try_get("avg(rating)")?;
|
rating_count: r.try_get("c")?,
|
||||||
|
average_rating: r.try_get("avg(rating)")?,
|
||||||
println!("Name: {name}, Ratings: {count}, Average: {avg}");
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(stats)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ use axum::{
|
|||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use tokio::sync::watch::Sender;
|
use tokio::sync::watch::Sender;
|
||||||
|
use tower_http::services::ServeDir;
|
||||||
|
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
|
||||||
@@ -31,11 +32,14 @@ pub async fn http_serve(database: &Database, mpris_producer: Sender<(String, Str
|
|||||||
};
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
.fallback_service(ServeDir::new("static"))
|
||||||
|
.route("/stats", get(get_stats))
|
||||||
.route("/", get(root))
|
.route("/", get(root))
|
||||||
.route("/rating/{rating}", get(cache_rating_only))
|
.route("/rating/{rating}", get(cache_rating_only))
|
||||||
.route("/userid/{user_id}", get(add_userid))
|
.route("/userid/{user_id}", get(add_userid))
|
||||||
.route("/usercard/{user_card}", get(add_userid_by_card))
|
.route("/usercard/{user_card}", get(add_userid_by_card))
|
||||||
.route("/{user_id}/{rating}", get(add_rating))
|
.route("/{user_id}/{rating}", get(add_rating))
|
||||||
|
.route("/adduser/{user_id}/{name}", get(add_user))
|
||||||
.with_state(shared_state);
|
.with_state(shared_state);
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
|
||||||
@@ -60,7 +64,26 @@ async fn add_rating(
|
|||||||
.user_add_rating(user_id, &interpret, &track, rating)
|
.user_add_rating(user_id, &interpret, &track, rating)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => (StatusCode::OK, "Done.").into_response(),
|
Ok(_) => {
|
||||||
|
eprintln!("ID: {user_id}, Rating: {rating}");
|
||||||
|
(StatusCode::OK, "Done.").into_response()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("HTTP error: {e}");
|
||||||
|
(StatusCode::BAD_REQUEST, e.to_string()).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_user(
|
||||||
|
Path((user_id, name)): Path<(i64, String)>,
|
||||||
|
State(shared): State<SharedState>,
|
||||||
|
) -> Response {
|
||||||
|
match shared.database.add_user_id(user_id, &name).await {
|
||||||
|
Ok(_) => {
|
||||||
|
eprintln!("ID: {user_id}, Name: {name}");
|
||||||
|
(StatusCode::OK, "Done.").into_response()
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("HTTP error: {e}");
|
eprintln!("HTTP error: {e}");
|
||||||
(StatusCode::BAD_REQUEST, e.to_string()).into_response()
|
(StatusCode::BAD_REQUEST, e.to_string()).into_response()
|
||||||
@@ -127,3 +150,17 @@ async fn add_userid_by_card(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_stats(State(shared): State<SharedState>) -> Response {
|
||||||
|
match shared.database.get_user_most_ratings().await {
|
||||||
|
Ok(stats) => {
|
||||||
|
// axum::Json automatically serializes the Vec<UserStats> into a JSON array
|
||||||
|
// and sets the correct "Content-Type: application/json" headers.
|
||||||
|
(StatusCode::OK, Json(stats)).into_response()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Database error: {e}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, "Failed to fetch stats").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
27
src/main.rs
27
src/main.rs
@@ -1,9 +1,8 @@
|
|||||||
mod database;
|
mod database;
|
||||||
mod http_server;
|
mod http_server;
|
||||||
mod player;
|
mod player;
|
||||||
mod userinterface;
|
|
||||||
|
|
||||||
use std::{env::args, sync::Arc, thread::sleep, time::Duration};
|
use std::{env::args, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use tokio::{join, sync::watch};
|
use tokio::{join, sync::watch};
|
||||||
|
|
||||||
@@ -19,8 +18,6 @@ async fn main() {
|
|||||||
// Create the tables on an empty database
|
// Create the tables on an empty database
|
||||||
db.create_tables().await.unwrap();
|
db.create_tables().await.unwrap();
|
||||||
|
|
||||||
let mut servermode = false;
|
|
||||||
|
|
||||||
if args().len() > 1 {
|
if args().len() > 1 {
|
||||||
// Add a new user
|
// Add a new user
|
||||||
let username = args().nth(1).unwrap();
|
let username = args().nth(1).unwrap();
|
||||||
@@ -42,8 +39,6 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.expect("Could not get ratings");
|
.expect("Could not get ratings");
|
||||||
return;
|
return;
|
||||||
} else if username == "-s" {
|
|
||||||
servermode = true;
|
|
||||||
} else {
|
} else {
|
||||||
let user_id = db.add_user(&username).await.expect("Could not add user");
|
let user_id = db.add_user(&username).await.expect("Could not add user");
|
||||||
println!("UserID for {username}: {user_id}");
|
println!("UserID for {username}: {user_id}");
|
||||||
@@ -67,7 +62,7 @@ async fn main() {
|
|||||||
let (mpris_tx, mut mpris_rx) = watch::channel((String::new(), String::new()));
|
let (mpris_tx, mut mpris_rx) = watch::channel((String::new(), String::new()));
|
||||||
|
|
||||||
let mpris_tx_http = mpris_tx.clone();
|
let mpris_tx_http = mpris_tx.clone();
|
||||||
tokio::spawn(async move {
|
std::thread::spawn(move || {
|
||||||
let player = player::MprisPlayer::new().expect("Could not create player");
|
let player = player::MprisPlayer::new().expect("Could not create player");
|
||||||
loop {
|
loop {
|
||||||
if let Ok((interpret, track)) = player.get_interpret_and_track() {
|
if let Ok((interpret, track)) = player.get_interpret_and_track() {
|
||||||
@@ -76,7 +71,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Use the std sleep here to avoid an await which will requires player to be Send.
|
// Use the std sleep here to avoid an await which will requires player to be Send.
|
||||||
sleep(Duration::from_millis(10));
|
std::thread::sleep(Duration::from_millis(1000));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,25 +81,9 @@ async fn main() {
|
|||||||
http_server::http_serve(&db_http, mpris_tx_http).await;
|
http_server::http_serve(&db_http, mpris_tx_http).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
if servermode {
|
|
||||||
eprintln!("Servermode...");
|
eprintln!("Servermode...");
|
||||||
let res = join!(http_handle);
|
let res = join!(http_handle);
|
||||||
if let Err(e) = res.0 {
|
if let Err(e) = res.0 {
|
||||||
eprintln!("{e}");
|
eprintln!("{e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
|
||||||
let (usernumber, userrating) = userinterface::get_user_rating(&db)
|
|
||||||
.await
|
|
||||||
.expect("Can not get user input");
|
|
||||||
let (interpret, track) = (*mpris_rx.borrow_and_update()).clone();
|
|
||||||
|
|
||||||
if let Err(e) = db
|
|
||||||
.user_add_rating(usernumber, &interpret, &track, userrating)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
eprintln!("{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
96
static/index.html
Normal file
96
static/index.html
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Live Music Ratings</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="container">
|
||||||
|
<h1>🎵 Live Leaderboard</h1>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<canvas id="ratingsChart"></canvas>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article>
|
||||||
|
<table id="stats-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Listener</th>
|
||||||
|
<th>Tracks Rated</th>
|
||||||
|
<th>Average Rating</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td aria-busy="true" colspan="3">Loading live stats...</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 3. Initialize the Chart (empty at first)
|
||||||
|
const ctx = document.getElementById('ratingsChart');
|
||||||
|
let ratingsChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: [], // This will hold user names
|
||||||
|
datasets: [{
|
||||||
|
label: 'Total Tracks Rated',
|
||||||
|
data: [], // This will hold the counts
|
||||||
|
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
||||||
|
borderColor: 'rgba(54, 162, 235, 1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
ticks: { stepSize: 1 } // Keep the Y axis as whole numbers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchLiveStats() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/stats');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// Update the Table
|
||||||
|
const tbody = document.querySelector('#stats-table tbody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
data.forEach(user => {
|
||||||
|
const row = `<tr>
|
||||||
|
<td><strong>${user.name}</strong></td>
|
||||||
|
<td>${user.rating_count}</td>
|
||||||
|
<td>⭐ ${user.average_rating.toFixed(2)}</td>
|
||||||
|
</tr>`;
|
||||||
|
tbody.innerHTML += row;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the Chart
|
||||||
|
// Map the JSON data into simple arrays for Chart.js
|
||||||
|
ratingsChart.data.labels = data.map(user => user.name);
|
||||||
|
ratingsChart.data.datasets[0].data = data.map(user => user.rating_count);
|
||||||
|
|
||||||
|
// Call update('none') to prevent the chart from doing a bouncy
|
||||||
|
// re-draw animation every 2 seconds when it polls.
|
||||||
|
ratingsChart.update('none');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching stats:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchLiveStats();
|
||||||
|
setInterval(fetchLiveStats, 2000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user