Skip to content

Commit

Permalink
Merge pull request #8 from vladkorotnev/develop
Browse files Browse the repository at this point in the history
Release v3.0
  • Loading branch information
vladkorotnev authored Aug 4, 2024
2 parents 23aa164 + 2cdf168 commit 7fc766f
Show file tree
Hide file tree
Showing 94 changed files with 9,399 additions and 2,399 deletions.
177 changes: 156 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,159 @@ Personal Information System OS (formerly Plasma Information Screen OS).

A somewhat portable relatively-stylish pixel-art clock/weather station.

![](docs/img/hero.jpg)

## A remark on the Morio Denki Plasma Display

**This display uses high voltage, which could be lethal!!**

The display comes from a bus or a train, supposedly.

It has the following labels on the PCBs:

* Morio Denki 6M06056 (the 8085-based control board I wasn't able to get running)
* MD 16101DS-CONT82 06 (the actual framebuffer/drive board)
* MD-24T-ADT (2) 8201 (the boards on the plasma tube itself)

Despite using a standard "HDD" Molex 4-pin connector for the drive board power, it expects +160V on the pin where normally +12V would be supplied. Take care not to mix up the power supplies. (Plugging in +12V into the plasma board doesn't seem to damage it. Plugging in +160V into an HDD, on the other hand...)

More detailed info is available in the following articles:

* На русском: https://habr.com/ru/companies/timeweb/articles/808805/
* 日本語で: https://elchika.com/article/b9f39c29-64aa-42ab-8f73-e6e27a72bd0e/
* Demo video: https://youtu.be/D4MiHmhhjeQ

You can also read the quest I went through trying to get it to run "in real time" at [EEVBlog Forums](https://www.eevblog.com/forum/repair/trying-to-figure-out-if-a-vfd-displaydriver-is-broken-(74-series-logic)/).

## Available widgets

* Clock

![](docs/img/widget/clock_pretty.png)

* Indoor temperature

![](docs/img/widget/indoor_pretty.png)

* Switchbot Meter temperature
* Weather (via [OpenWeatherMap](https://openweathermap.org/))

![](docs/img/widget/weather.gif)

* Weather Effect (raining and thunder on idle screen when rain/thunderstorm outside)

![](docs/img/widget/rain.gif)

* Word of the Day (via [Wordnik](https://wordnik.com/))

![](docs/img/widget/word_pretty.png)

* Foobar2000 (via [foo_controlverver](https://github.com/vladkorotnev/foo_controlserver/tree/fix-streams))

![](docs/img/widget/foo_pretty.png)

* Remaining sleep time (when using alarm app)

![](docs/img/widget/sleep_pretty.png)

## Available apps

* Idle (home screen)
* Timer

![](docs/img/app/timer.gif)
![](docs/img/app/timer_melo_pretty.png)
* Stopwatch

![](docs/img/app/stopwatch.gif)
* Alarm (Smart Wake Up on devices with motion sensor)

![](docs/img/app/alarm_pretty.png)
![](docs/img/app/alarm2_pretty.png)
![](docs/img/app/alarm.gif)

*(blinking lights warning!)*
* Weighing Scale (via Wii Balance Board)

![](docs/img/app/weighing.gif)
* Settings

![](docs/img/app/settings.gif)

## Available chime tones

**(All chime tones are covers adapted for single channel beeper by DJ AKASAKA)**

* Simple beep
* PC-98 Boot Chime
* Russ28 (Русь 28) Hourly Chime (poorly timed Beethoven's 5th Symphony)
* Some mid-2000s Shtrikh-M (Штрих-М) POS Boot chime
* [A.M. - Arise](https://youtu.be/cuOVP6pJ9Ww?t=264): [MIDI](helper/chimes/arise.mid)
* [Caramell - Caramelldansen](https://youtu.be/o6Zp4cW8w8A?t=51): [MIDI](helper/chimes/caramelldansen.mid)
* [BôA - Duvet](https://youtu.be/Uoox9fpmDP0) (simple version): [MIDI](helper/chimes/duvet.mid)
* [T-Square - Truth](https://youtu.be/FZaUPGjjA4c?t=77): [MIDI](helper/chimes/truth.mid)
* [Kosaki Satoru - Haruhi no Omoi](https://www.youtube.com/watch?v=KMKoyHKYSNk): [MIDI](helper/chimes/haruhi_no_omoi.mid)
* [WAVE and DRAX - When Present Is Past](https://modarchive.org/index.php?request=view_by_moduleid&query=140039): [MIDI](helper/chimes/when_present_is_past.mid)
* [Kawada Mami - Hishoku no Sora](https://www.youtube.com/watch?v=FNl1ud7KxtI): [MIDI](helper/chimes/hishoku.mid)
* [Hirano Aya - Bouken Desho Desho](https://www.youtube.com/watch?v=C337shIT9LI): [MIDI](helper/chimes/bouken.mid)
* [Magome Togoshi, Shinji Orito - Gentle Jena](https://www.youtube.com/watch?v=lR4dw_B423E): [MIDI](helper/chimes/gentlejena.mid)
* [OMEGA - Gammapolisz](https://www.youtube.com/watch?v=XCqdrQxMrxI): [MIDI](helper/chimes/gammapolisz.mid)
* [? - Like The Wind (TMMS)](https://www.youtube.com/watch?v=uYpkri8Kv2E): [MIDI](helper/chimes/like_the_wind.mid)
* [NightRadio - Waiting Freqs](https://www.youtube.com/watch?v=_0MBreuq94Y): [MIDI](helper/chimes/waiting_freqs.mid)
* [NightRadio - The Way](https://nightradio.bandcamp.com/track/the-way): [MIDI](helper/chimes/the_way.mid)
* [Dougal & Gammer — Guitar Hero](https://youtu.be/ID4pO9epHPA?t=89): [MIDI](helper/chimes/gtrhero.mid)
* [蜂屋ななし — シャボン](https://youtu.be/UHRlXm_tV8o?t=41): [MIDI](helper/chimes/syabon.mid)
* [Takeshi Abo — Gate of Steiner](https://www.youtube.com/watch?v=2Q9MqL83FeE): [MIDI](helper/chimes/steiner.mid)
* [deadballP — 永遠に続く五線譜](https://www.youtube.com/watch?v=p1sh68qk4Nk): [MIDI](helper/chimes/towa.mid)
* [Cream puff — Mermaid girl](https://youtu.be/AaUMvgfHpUo?t=16): [MIDI](helper/chimes/mermgrl.mid)
* [Brisk & Trixxy — Eye Opener](https://youtu.be/81QqHUpyBhg?t=83): [MIDI](helper/chimes/eye_opener.mid)
* [Hiroyuki Oshima - The Ark Awakes From The Sea Of Chaos](https://www.youtube.com/watch?app=desktop&v=cB7eevDk1s0): [MIDI](helper/chimes/ark.mid)
* [Timbaland - Give It To Me](https://youtube.com/watch?v=RgoiSJ23cSc) also known as [Skibidi Toilet](https://youtu.be/6dMjCa0nqK0): [MIDI](helper/chimes/skibidi_toilet.mid)
* [PinocchioP - God-ish (神っぽいな)](https://www.youtube.com/watch?v=EHBFKhLUVig): [MIDI](helper/chimes/kamippoina.mid)

MIDI to sequencer conversion tool (supports note events in one track only, track end event, and comment event): [midi_to_chime](helper/midi_to_chime.py)

8 bit 8 kHz wave to RLE sample conversion tool (not even reading the header, so very jank): [pwm.py](helper/pwm.py)

## Remote Control Server

There is a remote control server you can enable in settings for debugging remotely when uploading firmware via OTA, or using an emulator without any screen and buttons.

Also included is a [primitive client](helper/remote-control.py) that has pretty poor performance, but allows recording GIFs and taking screenshots. All of the screenshots and GIFs in this readme were made that way.

https://github.com/user-attachments/assets/b1d1ee7e-b5f3-4800-a475-d44ae876bf7e

### Usage

1. Enable "Remote Control Server" under Settings → Display.
2. Save and Restart PIS-OS
3. Run `python ./helper/remote-control.py <CLOCK-IP>` on your computer. Port 3939 must be accessible.

### Protocol

The protocol is very simple.

The control client sends a control packet to the clock via UDP:

```
{
uint16_t magic = 0x3939;
key_id_t pressed = (set bits of those keys that were pressed since last transaction);
key_id_t released = (set bits of those keys that were released since last transaction);
}
```

After that the client should expect a UDP packet from the clock with the format:

```
{
uint16_t magic = 0x8888;
uint16_t display_width;
uint16_t display_height;
... remainder: bitmap data in fanta buffer format
}
```

## System Requirements

The basic configuration without any bluetooth functionality (no Switchbot or Balance Board integration) seems to work just fine on an ESP32 WROOM. However to be less limited by RAM size in further features I've decided to make WROVER the requirement, so further versions are not guaranteed to run on WROOM.
Expand All @@ -23,7 +176,7 @@ The basic configuration without any bluetooth functionality (no Switchbot or Bal

### Speaker (at least one required)

* Piezo speaker ([driver](src/sound/beeper.cpp), [music](src/sound/melodies.cpp))
* Piezo speakers: *now with 1-bit DMA polyphony!* ([driver](src/sound/beeper.cpp), [music](src/sound/melodies.cpp), [sequencer](src/sound/sequencer.cpp))

### Haptics (WIP)

Expand Down Expand Up @@ -57,24 +210,6 @@ The basic configuration without any bluetooth functionality (no Switchbot or Bal

* Wii Balance Board. Set feature flag `HAS_BALANCE_BOARD_INTEGRATION`. [Driver](src/service/balance_board.cpp), based upon [code by Sasaki Takeru](https://github.com/takeru/Wiimote/tree/d81319c62ac5931da868cc289386a6d4880a4b15), requires WROVER module

## Morio Denki Plasma Display Info

**This display uses high voltage, which could be lethal!!**

The display comes from a bus or a train, supposedly.

It has the following labels on the PCBs:

* Morio Denki 6M06056 (the 8085-based control board I wasn't able to get running)
* MD 16101DS-CONT82 06 (the actual framebuffer/drive board)
* MD-24T-ADT (2) 8201 (the boards on the plasma tube itself)

Despite using a standard "HDD" Molex 4-pin connector for the drive board power, it expects +160V on the pin where normally +12V would be supplied. Take care not to mix up the power supplies. (Plugging in +12V into the plasma board doesn't seem to damage it. Plugging in +160V into an HDD, on the other hand...)

More detailed info is available in the following articles:

* На русском: https://habr.com/ru/companies/timeweb/articles/808805/
* 日本語で: https://elchika.com/article/b9f39c29-64aa-42ab-8f73-e6e27a72bd0e/
* Demo video: https://youtu.be/D4MiHmhhjeQ
----

You can also read the quest I went through trying to get it to run "in real time" at [EEVBlog Forums](https://www.eevblog.com/forum/repair/trying-to-figure-out-if-a-vfd-displaydriver-is-broken-(74-series-logic)/).
by Genjitsu Labs / akasaka, 2024.
Binary file added docs/img/app/alarm.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/app/alarm2_pretty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/app/alarm_pretty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/app/settings.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/app/stopwatch.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/app/timer.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/app/timer_melo_pretty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/app/weighing.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/hero.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/pisosremote.mp4
Binary file not shown.
Binary file added docs/img/widget/clock_pretty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/widget/foo_pretty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/widget/indoor_pretty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/widget/rain.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/widget/sleep_pretty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/widget/weather.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/widget/word_pretty.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified helper/chimes/arise.mid
Binary file not shown.
Binary file added helper/chimes/ark.mid
Binary file not shown.
Binary file modified helper/chimes/bouken.mid
Binary file not shown.
Binary file modified helper/chimes/duvet.mid
Binary file not shown.
Binary file modified helper/chimes/gammapolisz.mid
Binary file not shown.
Binary file modified helper/chimes/gentlejena.mid
Binary file not shown.
Binary file added helper/chimes/kamippoina.mid
Binary file not shown.
Binary file added helper/chimes/skibidi_toilet.mid
Binary file not shown.
73 changes: 46 additions & 27 deletions helper/midi_to_chime.py
Original file line number Diff line number Diff line change
@@ -1,54 +1,73 @@
#!/usr/bin/env python3

# this is very jank, do not expect it to work as is
# it worked for some melodies though

import pdb
from sys import argv
from mido import MidiFile
import freq_note_converter

mid = MidiFile(argv[1])
name = argv[2]

last_time = 0
evts = [] # of (freq in hz or 0, delay in ms)
ended = False

class Event():
def __init__(self, kind, chan, arg):
self.kind = kind
self.chan = chan
self.arg = arg

def __str__(self):
return f" {{{self.kind}, {str(self.chan)}, {str(int(self.arg))}}},"

class Comment():
def __init__(self, s):
self.content = s
self.kind = "REM"

def __str__(self):
return f" /* {self.content} */"

evts = []

def prev_note_off_event(chan):
for i in range(1,len(evts)+1):
e = evts[-i]
if e.kind == "FREQ_SET" and e.arg == 0 and e.chan == chan:
return e
elif e.kind == "DELAY":
return None
return None

for msg in mid:
print(msg)
if msg.type == "note_on" or msg.type == "note_off":
print(msg)
if msg.time > 0 and len(evts) > 0 and evts[-1][1] == 0:
evts[-1][1] = int(msg.time * 1000)
if msg.time > 0.005:
evts.append(Event("DELAY", 0, msg.time * 1000))
if msg.type == "note_on" and msg.velocity > 0:
evts.append([int(freq_note_converter.from_note_index(msg.note).freq), 0, ""])
existing_evt = prev_note_off_event(msg.channel)
if existing_evt is not None:
existing_evt.arg = freq_note_converter.from_note_index(msg.note).freq
else:
evts.append(Event("FREQ_SET", msg.channel, freq_note_converter.from_note_index(msg.note).freq))
else:
# note off
evts.append([0, 0, ""])
evts.append(Event("FREQ_SET", msg.channel, 0))
elif msg.type == "end_of_track":
print(msg)
if ended:
raise Exception("WTF, already ended")
ended = True
if evts[-1][0] == 0:
# pause exists, just extend it
evts[-1][1] = int(msg.time * 1000)
else:
evts.append([0, int(msg.time*1000), ""])
evts.append(Event("DELAY", 0, msg.time * 1000))
elif msg.type == "marker":
evts[-1][2] = msg.text

if msg.time > 0.005:
evts.append(Event("DELAY", 0, msg.time * 1000))
evts.append(Comment(msg.text))
if msg.text == "LOOP":
evts.append(Event("LOOP_POINT_SET", 0, 0))

print(evts)

print("static const melody_item_t "+name+"_data[] = {")
i = 0
while i < len(evts):
if evts[i][0] != 0 or evts[i][1] != 0:
print(" {"+str(evts[i][0])+", "+str(evts[i][1])+"}, ")
if evts[i][2] != "":
print(" ")
print(" // " + evts[i][2])
i+=1
for e in evts:
print(str(e))
print("};")

print("const melody_sequence_t "+name+" = MELODY_OF("+name+"_data);")
33 changes: 33 additions & 0 deletions helper/pretty-screenshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/bin/env python
# -*- coding: utf-8 -*-

import sys
from pathlib import Path

IMG_IN=sys.argv[1]
IMG_OUT=sys.argv[2] if len(sys.argv) > 2 else str(Path(IMG_IN).with_suffix(''))+"_pretty.png"

PIX_COLOR=(0, 210, 242)
PIX_SIZE=4
PIX_PITCH_H=1
PIX_PITCH_W=1

from PIL import Image, ImageFilter
im = Image.open(IMG_IN, 'r')
src = im.load()

width, height = im.size

out_img = Image.new(mode="RGB", size=(width * (PIX_SIZE + PIX_PITCH_H), height * (PIX_SIZE + PIX_PITCH_H)))

for y in range(height):
for x in range(width):
v = src[x, y]
for dy in range(PIX_SIZE):
for dx in range(PIX_SIZE):
out_x = x * (PIX_SIZE + PIX_PITCH_W) + dx
out_y = y * (PIX_SIZE + PIX_PITCH_H) + dy
if out_x < out_img.width and out_y < out_img.height:
out_img.putpixel((out_x, out_y), PIX_COLOR if v > 0 else (0, 0, 0))

out_img.filter(ImageFilter.GaussianBlur(radius=PIX_SIZE/10)).save(IMG_OUT)
60 changes: 60 additions & 0 deletions helper/pwm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#-*- coding: utf-8 -*-

# Super jank converter from 8bit WAV into RLE PWM + preview
# Not reading the header or anything hence super jank

import sys

MARGIN = 4

fname = sys.argv[1]
sname = sys.argv[2]
oname = sys.argv[3] if len(sys.argv) >= 4 else None
sdata = open(fname, 'rb').read()
outf = None
if oname is not None:
outf = open(oname, 'wb')
outf.write(sdata[:0x28])

sdata = sdata[0x28::]
i = 0
min = 999
max = 0
sts = 0xFF
last_sts = 0xFF
rle_buf = [0]

def median(data):
x = list(data)
x.sort()
mid = len(x) // 2
return (x[mid] + x[~mid]) / 2.0

med = median(sdata)
print("Median", med)
HIGH = med + MARGIN
LO = med - MARGIN

while i < len(sdata):
curSample = sdata[i]
if curSample >= HIGH:
sts = 255
elif curSample <= LO:
sts = 1
if curSample < min and curSample > 0:
min = curSample
if curSample > max and curSample > 0:
max = curSample
if outf is not None:
outf.write(bytes([sts, sts]))
if sts != last_sts:
rle_buf.append(0)
last_sts = sts
if rle_buf[-1] == 255:
rle_buf.append(0)
rle_buf.append(0)
rle_buf[-1] += 1
i += 2

print(f"static const uint8_t {sname}_rle_data[] = {{" + str(rle_buf)[1::][:-1:] + "};")
print(f"static const rle_sample_t {sname} = {{ .sample_rate = 4000, .root_frequency = 524 /* C5 */, .rle_data = {sname}_rle_data, .length = {len(rle_buf)} }};")
Loading

0 comments on commit 7fc766f

Please sign in to comment.