Skip to content

Commit ffd2a23

Browse files
committed
change the palette from the interface
1 parent 5fdd18a commit ffd2a23

File tree

13 files changed

+218
-6
lines changed

13 files changed

+218
-6
lines changed

backend/app/api/frames.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,7 @@ async def api_frame_import(
11061106
"controlCode": "control_code",
11071107
"network": "network",
11081108
"agent": "agent",
1109+
"palette": "palette",
11091110
"scenes": "scenes",
11101111
}
11111112
for src, dest in mapping.items():

backend/app/models/frame.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class Frame(Base):
5858
gpio_buttons = mapped_column(JSON, nullable=True)
5959
network = mapped_column(JSON, nullable=True)
6060
agent = mapped_column(JSON, nullable=True)
61+
palette = mapped_column(JSON, nullable=True)
6162

6263
# not used
6364
apps = mapped_column(JSON, nullable=True)
@@ -102,6 +103,7 @@ def to_dict(self):
102103
'gpio_buttons': self.gpio_buttons,
103104
'network': self.network,
104105
'agent': self.agent,
106+
'palette': self.palette,
105107
'last_successful_deploy': self.last_successful_deploy,
106108
'last_successful_deploy_at': self.last_successful_deploy_at.replace(tzinfo=timezone.utc).isoformat() if self.last_successful_deploy_at else None,
107109
}
@@ -246,6 +248,7 @@ def get_frame_json(db: Session, frame: Frame) -> dict:
246248
for button in (frame.gpio_buttons or [])
247249
if int(button.get("pin", 0)) > 0
248250
],
251+
"palette": frame.palette or {},
249252
"controlCode": {
250253
"enabled": frame.control_code.get('enabled', 'true') == 'true',
251254
"position": frame.control_code.get('position', 'top-right'),

backend/app/schemas/frames.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class FrameBase(BaseModel):
4141
gpio_buttons: Optional[List[Dict[str, Any]]]
4242
network: Optional[Dict[str, Any]]
4343
agent: Optional[Dict[str, Any]]
44+
palette: Optional[Dict[str, Any]]
4445
last_successful_deploy: Optional[Dict[str, Any]]
4546
last_successful_deploy_at: Optional[datetime]
4647
active_connections: Optional[int] = None
@@ -90,6 +91,7 @@ class FrameUpdateRequest(BaseModel):
9091
gpio_buttons: Optional[List[Dict[str, Any]]] = None
9192
network: Optional[Dict[str, Any]] = None
9293
agent: Optional[Dict[str, Any]] = None
94+
palette: Optional[Dict[str, Any]] = None
9395
next_action: Optional[str] = None
9496

9597
class FrameLogsResponse(BaseModel):
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""palette
2+
3+
Revision ID: 1a4ece62d617
4+
Revises: d1257bdc91fd
5+
Create Date: 2025-06-21 00:46:44.593367
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
from sqlalchemy.dialects import sqlite
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '1a4ece62d617'
14+
down_revision = 'd1257bdc91fd'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade():
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.add_column('frame', sa.Column('palette', sqlite.JSON(), nullable=True))
22+
# ### end Alembic commands ###
23+
24+
25+
def downgrade():
26+
# ### commands auto generated by Alembic - please adjust! ###
27+
op.drop_column('frame', 'palette')
28+
# ### end Alembic commands ###

frameos/src/drivers/waveshare/waveshare.nim

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import pixie, json, times, locks
1+
import pixie, json, times, locks, options, sequtils
22

33
import frameos/types
44
import frameos/utils/image
@@ -12,6 +12,7 @@ type Driver* = ref object of FrameOSDriver
1212
height: int
1313
lastImageData: seq[ColorRGBX]
1414
lastRenderAt: float
15+
palette: Option[seq[(int, int, int)]]
1516

1617
var
1718
lastFloatImageLock: Lock
@@ -53,7 +54,23 @@ proc init*(frameOS: FrameOS): Driver =
5354
logger: logger,
5455
width: width,
5556
height: height,
57+
palette: none(seq[(int, int, int)]),
5658
)
59+
60+
if waveshareDriver.colorOption == ColorOption.SpectraSixColor and len(frameOS.frameConfig.palette.colors) == 6:
61+
let c = frameOS.frameConfig.palette.colors
62+
result.palette = some(@[
63+
(c[0][0], c[0][1], c[0][2]),
64+
(c[1][0], c[1][1], c[1][2]),
65+
(c[2][0], c[2][1], c[2][2]),
66+
(c[3][0], c[3][1], c[3][2]),
67+
(999, 999, 999),
68+
(c[4][0], c[4][1], c[4][2]),
69+
(c[5][0], c[5][1], c[5][2]),
70+
])
71+
else:
72+
result.palette = some(spectra6ColorPalette)
73+
5774
except Exception as e:
5875
logger.log(%*{"event": "driver:waveshare",
5976
"error": "Failed to initialize driver", "exception": e.msg,
@@ -136,7 +153,7 @@ proc renderSevenColor*(self: Driver, image: Image) =
136153
waveshareDriver.renderImage(pixels)
137154

138155
proc renderSpectraSixColor*(self: Driver, image: Image) =
139-
let pixels = ditherPaletteIndexed(image, spectra6ColorPalette)
156+
let pixels = ditherPaletteIndexed(image, if self.palette.isSome(): self.palette.get() else: spectra6ColorPalette)
140157
setLastPixels(pixels)
141158
self.notifyImageAvailable()
142159
waveshareDriver.renderImage(pixels)

frameos/src/frameos/config.nim

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,24 @@ proc loadNetwork*(data: JsonNode): NetworkConfig =
6969
wifiHostpotTimeoutSeconds: data{"wifiHotspotTimeoutSeconds"}.getFloat(600),
7070
)
7171

72+
proc loadPalette*(data: JsonNode): PaletteConfig =
73+
if data == nil or data.kind != JObject or data["colors"] == nil or data["colors"].kind != JArray:
74+
result = PaletteConfig(colors: @[])
75+
else:
76+
result = PaletteConfig(colors: @[])
77+
for color in data["colors"].items:
78+
try:
79+
let color = parseHtmlColor(color.getStr())
80+
result.colors.add((
81+
int(color.r * 255),
82+
int(color.g * 255),
83+
int(color.b * 255),
84+
))
85+
except:
86+
echo "Warning: Invalid color in palette: ", color.getStr()
87+
result.colors = @[]
88+
return result
89+
7290
proc loadAgent*(data: JsonNode): AgentConfig =
7391
if data == nil or data.kind != JObject:
7492
result = AgentConfig(agentEnabled: false)
@@ -106,6 +124,7 @@ proc loadConfig*(filename: string = "frame.json"): FrameConfig =
106124
gpioButtons: loadGPIOButtons(data{"gpioButtons"}),
107125
controlCode: loadControlCode(data{"controlCode"}),
108126
network: loadNetwork(data{"network"}),
127+
palette: loadPalette(data{"palette"}),
109128
)
110129
if result.assetsPath.endswith("/"):
111130
result.assetsPath = result.assetsPath.strip(leading = false, trailing = true, chars = {'/'})

frameos/src/frameos/frameos.nim

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ proc start*(self: FrameOS) {.async.} =
4646
"logToFile": self.frameConfig.logToFile,
4747
"debug": self.frameConfig.debug,
4848
"timeZone": self.frameConfig.timeZone,
49-
"gpioButtons": self.frameConfig.gpioButtons,
49+
"gpioButtons": self.frameConfig.gpioButtons
5050
}}
5151
self.logger.log(message)
5252
netportal.setLogger(self.logger)

frameos/src/frameos/types.nim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type
2727
controlCode*: ControlCode
2828
network*: NetworkConfig
2929
agent*: AgentConfig
30+
palette*: PaletteConfig
3031

3132
GPIOButton* = ref object
3233
pin*: int
@@ -56,6 +57,9 @@ type
5657
agentRunCommands*: bool
5758
agentSharedSecret*: string
5859

60+
PaletteConfig* = ref object
61+
colors*: seq[(int, int, int)]
62+
5963
FrameSchedule* = ref object
6064
events*: seq[ScheduledEvent]
6165

frontend/src/devices.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Option } from './components/Select'
2+
import { Palette } from './types'
23

34
// To generate a new version:
45
// cd backend && python3 list_devices.py
@@ -98,3 +99,63 @@ export const devices: Option[] = [
9899
{ value: 'waveshare.EPD_13in3k', label: 'Waveshare 13.3" (K) 960x680 Black/White' },
99100
{ value: 'waveshare.EPD_13in3e', label: 'Waveshare 13.3" (E) 1600x1200 Spectra 6 Color' },
100101
]
102+
103+
const colorNames = ['Black', 'White', 'Yellow', 'Red', 'Blue', 'Green']
104+
export const spectraPalettes: Palette[] = [
105+
{
106+
name: 'Default',
107+
colorNames,
108+
colors: [
109+
'#000000', // Black
110+
'#ffffff', // White
111+
'#fff338', // Yellow
112+
'#bf0000', // Red
113+
'#6440ff', // Blue
114+
'#438a1c', // Green
115+
],
116+
},
117+
{
118+
name: 'Desaturated',
119+
colorNames,
120+
colors: [
121+
'#000000', // Black
122+
'#ffffff', // White
123+
'#ffff00', // Yellow
124+
'#ff0000', // Red
125+
'#0000ff', // Blue
126+
'#00ff00', // Green
127+
],
128+
},
129+
{
130+
name: 'Pimoroni Saturated',
131+
colorNames,
132+
colors: [
133+
'#000000', // Black
134+
'#a1a4a5', // Gray
135+
'#d0be47', // Yellow
136+
'#9c484b', // Red
137+
'#3d3b5e', // Blue
138+
'#3a5b46', // Green
139+
],
140+
},
141+
{
142+
name: 'Measured',
143+
colorNames,
144+
colors: [
145+
'#3C3542', // Black
146+
'#DCDDD4', // White
147+
'#EDD600', // Yellow
148+
'#C12117', // Red
149+
'#2461C5', // Blue
150+
'#548B79', // Green
151+
],
152+
},
153+
]
154+
155+
// SATURATED_PALETTE = [
156+
157+
export const withCustomPalette: Record<string, Palette> = {
158+
'waveshare.EPD_13in3e': spectraPalettes[0],
159+
'waveshare.EPD_7in3e': spectraPalettes[0],
160+
'waveshare.EPD_4in0e': spectraPalettes[0],
161+
}

frontend/src/scenes/frame/Frame.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ export function Frame(props: FrameSceneProps) {
2929
restartAgent,
3030
} = useActions(frameLogic(frameLogicProps))
3131
const { openLogs } = useActions(panelsLogic(frameLogicProps))
32-
const canDeployAgent = !!(frame.agent && frame.agent.agentEnabled && frame.agent.agentSharedSecret)
33-
const agentExtra = canDeployAgent ? (frame.agent?.agentRunCommands ? ' (via agent)' : ' (via ssh)') : ''
32+
const canDeployAgent = !!(frame?.agent && frame.agent.agentEnabled && frame.agent.agentSharedSecret)
33+
const agentExtra = canDeployAgent ? (frame?.agent?.agentRunCommands ? ' (via agent)' : ' (via ssh)') : ''
3434

3535
return (
3636
<BindLogic logic={frameLogic} props={frameLogicProps}>

0 commit comments

Comments
 (0)