Skip to content

Commit 2919220

Browse files
authored
feat: Add OLED Display Instrument (#3395)
1 parent c8c50f5 commit 2919220

11 files changed

Lines changed: 1792 additions & 5 deletions
65.2 KB
Loading

assets/images/oled_display.png

266 KB
Loading

lib/constants.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ List<String> instrumentIcons = [
2828
'assets/icons/gyroscope_logo.png',
2929
'assets/icons/thermometer_logo.png',
3030
'assets/icons/robotic_arm.png',
31-
'assets/icons/tile_icon_gas.png',
32-
'assets/icons/tile_icon_gas.png',
33-
'assets/icons/tile_icon_gas.png',
31+
'assets/icons/tile_icon_gas.png', // Gas Sensor
32+
'assets/icons/tile_icon_gas.png', // Sound Meter
33+
'assets/icons/tile_icon_oled_screen.png',
3434
];
3535

3636
List<String> instrumentNames = [
@@ -48,6 +48,6 @@ List<String> instrumentNames = [
4848
appLocalizations.thermometer.toLowerCase(),
4949
appLocalizations.roboticArmTitle.toLowerCase(),
5050
appLocalizations.gasSensor.toLowerCase(),
51-
appLocalizations.dustSensor.toLowerCase(),
52-
appLocalizations.soundMeter.toLowerCase()
51+
appLocalizations.soundMeter.toLowerCase(),
52+
appLocalizations.oledDisplayTitle.toLowerCase(),
5353
];

lib/l10n/app_en.arb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,5 +774,36 @@
774774
"tvoc" : "TVOC",
775775
"ppb" : "ppb",
776776
"concentrationPpm" : "Concentration (PPM)",
777+
"oledDisplayTitle": "OLED Display",
778+
"oledDisplayIntro": "The OLED Display is a compact monochrome screen that communicates with the PSLab device via the I2C protocol. It allows you to render text, draw live shapes, and display custom GIFs.",
779+
"oledDisplayConnection": "To use this instrument, connect the OLED's VCC, GND, SDA, and SCL pins directly to the corresponding 3.3V, GND, SDA, and SCL pins on the PSLab board's I2C header.",
780+
"undo": "Undo",
781+
"redo": "Redo",
782+
"clearCanvas": "Clear Canvas",
783+
"typeAndSend": "Type and press send...",
784+
"gifPlaying": "GIF Playing...",
785+
"pencil": "Pencil",
786+
"eraser": "Eraser",
787+
"uploadImage": "Upload Static Image",
788+
"stopGif": "Stop GIF",
789+
"uploadGif": "Upload GIF",
790+
"line": "Line",
791+
"rectangle": "Rectangle",
792+
"grid": "Grid",
793+
"circle": "Circle",
794+
"semiCircle": "Semi-Circle",
795+
"triangle": "Triangle",
796+
"diamond": "Diamond",
797+
"pentagon": "Pentagon",
798+
"hexagon": "Hexagon",
799+
"star": "Star",
800+
"heart": "Heart",
801+
"arrow": "Arrow",
802+
"cross": "Cross",
803+
"checkmark": "Checkmark",
804+
"canvas": "CANVAS",
805+
"configure": "CONFIGURE",
806+
"liveRender": "Live Render",
807+
"snapshotSaved": "Canvas Snapshot Saved to Logs!",
777808
"airQuality" : "Air Quality"
778809
}

lib/main.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import 'package:pslab/view/soundmeter_screen.dart';
3636
import 'package:pslab/view/thermometer_screen.dart';
3737
import 'package:pslab/view/wave_generator_screen.dart';
3838
import 'package:pslab/view/experiments_screen.dart';
39+
import 'package:pslab/view/oled_display_screen.dart';
3940
import 'constants.dart';
4041

4142
void main(List<String> args) async {
@@ -156,6 +157,8 @@ class MyApp extends StatelessWidget {
156157
instrumentIcons: instrumentIcons,
157158
),
158159
),
160+
'/oledDisplay': (context) =>
161+
const _LocaleAware(child: OledDisplayScreen()),
159162
},
160163
);
161164
},

lib/others/oled_display.dart

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import 'package:pslab/communication/peripherals/i2c.dart';
2+
import 'package:pslab/communication/science_lab.dart';
3+
import 'package:pslab/others/logger_service.dart';
4+
5+
enum OledModel {
6+
sh1106_128x64,
7+
ssd1306_128x64,
8+
ssd1306_128x32,
9+
}
10+
11+
class OLED {
12+
static const String tag = "OLED_Hardware";
13+
static const int address = 0x3C;
14+
15+
static const int _commandMode = 0x00;
16+
static const int _dataMode = 0x40;
17+
18+
final I2C i2c;
19+
final OledModel model;
20+
21+
late final int _pages;
22+
late final int _columnOffset;
23+
24+
List<int> _lastBuffer = List.filled(1024, -1);
25+
26+
bool _isBusy = false;
27+
28+
OLED._(this.i2c, this.model) {
29+
_pages = (model == OledModel.ssd1306_128x32) ? 4 : 8;
30+
_columnOffset = (model == OledModel.sh1106_128x64) ? 2 : 0;
31+
}
32+
33+
static Future<OLED> create(
34+
I2C i2c, ScienceLab scienceLab, OledModel model) async {
35+
final display = OLED._(i2c, model);
36+
await display._initialize(scienceLab);
37+
return display;
38+
}
39+
40+
Future<void> _initialize(ScienceLab scienceLab) async {
41+
if (!scienceLab.isConnected()) throw Exception("ScienceLab device absent");
42+
43+
try {
44+
await i2c.config(400000);
45+
46+
List<int> initSequence = [
47+
0xAE,
48+
0xD5,
49+
0x80,
50+
0xA8,
51+
_pages == 4 ? 0x1F : 0x3F,
52+
0xD3,
53+
0x00,
54+
0x40,
55+
];
56+
57+
if (model == OledModel.sh1106_128x64) {
58+
initSequence.addAll([0xAD, 0x8B]);
59+
} else {
60+
initSequence.addAll([0x8D, 0x14]);
61+
}
62+
63+
initSequence.addAll([
64+
0x20,
65+
0x02,
66+
0xA1,
67+
0xC8,
68+
0xDA,
69+
_pages == 4 ? 0x02 : 0x12,
70+
0x81,
71+
0x7F,
72+
0xD9,
73+
model == OledModel.sh1106_128x64 ? 0x1F : 0xF1,
74+
0xDB,
75+
0x40,
76+
0xA4,
77+
0xA6,
78+
0xAF
79+
]);
80+
81+
for (int cmd in initSequence) {
82+
await i2c.write(address, [cmd], _commandMode);
83+
}
84+
85+
await Future.delayed(const Duration(milliseconds: 50));
86+
await clearDisplay();
87+
} catch (e) {
88+
logger.e("[$tag] Fatal initialization error: $e");
89+
rethrow;
90+
}
91+
}
92+
93+
Future<void> _setPosition(int column, int page) async {
94+
int physicalColumn = column + _columnOffset;
95+
await i2c.write(address, [0xB0 + page], _commandMode);
96+
await i2c.write(address, [physicalColumn & 0x0F], _commandMode);
97+
await i2c.write(address, [0x10 | (physicalColumn >> 4)], _commandMode);
98+
}
99+
100+
Future<void> clearDisplay() async {
101+
if (_isBusy) return;
102+
_isBusy = true;
103+
104+
try {
105+
for (int page = 0; page < _pages; page++) {
106+
await _setPosition(0, page);
107+
108+
await i2c.write(address, List.filled(128, 0x00), _dataMode);
109+
}
110+
111+
_lastBuffer = List.filled(1024, 0);
112+
} catch (e) {
113+
logger.e("[$tag] Error during clearDisplay: $e");
114+
} finally {
115+
_isBusy = false;
116+
}
117+
}
118+
119+
Future<void> sendFrameBuffer(List<int> buffer) async {
120+
if (buffer.length != 1024) return;
121+
122+
if (_isBusy) return;
123+
_isBusy = true;
124+
125+
try {
126+
for (int page = 0; page < _pages; page++) {
127+
int startIdx = page * 128;
128+
int firstCol = -1;
129+
int lastCol = -1;
130+
131+
for (int i = 0; i < 128; i++) {
132+
if (buffer[startIdx + i] != _lastBuffer[startIdx + i]) {
133+
if (firstCol == -1) firstCol = i;
134+
lastCol = i;
135+
}
136+
}
137+
138+
if (firstCol != -1) {
139+
int length = lastCol - firstCol + 1;
140+
141+
try {
142+
await _setPosition(firstCol, page);
143+
144+
List<int> bytes = buffer.sublist(
145+
startIdx + firstCol, startIdx + firstCol + length);
146+
await i2c.write(address, bytes, _dataMode);
147+
} catch (e) {
148+
_lastBuffer = List.filled(1024, -1);
149+
_isBusy = false;
150+
return;
151+
}
152+
153+
for (int i = firstCol; i <= lastCol; i++) {
154+
_lastBuffer[startIdx + i] = buffer[startIdx + i];
155+
}
156+
}
157+
}
158+
} catch (e) {
159+
_lastBuffer = List.filled(1024, -1);
160+
} finally {
161+
_isBusy = false;
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)